Use hidden="until-found" for collapsible content so that browser find-in-page, assistive tech, and search engines can still reach the text and auto-expand it.
What it is #
hidden="until-found"
is a value of the global hidden
attribute defined in the HTML Standard. An element marked this way renders as if hidden — it takes up no visible space — but the browser’s find-in-page (Ctrl/Cmd+F), the fragment-directive scroll-to-text, and scrollIntoView()
still walk through it. When a match is found inside, the browser fires a beforematch
event on the element, removes the hidden
attribute, and scrolls the match into view.
The browser’s user-agent stylesheet applies content-visibility: hidden
to elements in the until-found state, and the find-in-page algorithm has a specific exception for that state — when a match would land inside the subtree, it walks the element, removes the hidden
attribute, and scrolls into view. Applying content-visibility: hidden directly in author CSS does not get you the same behaviour. Per the CSS Containment specification, contents in that state are skipped from rendering
andare not visible to screen readers, find-in-page, or other tools. The reachability is a property of the HTML attribute, not the CSS property.
Why it matters #
display: none
removes content from the accessibility tree and from find-in-page entirely. Accordion or tab patterns that hide panels withdisplay: none
are invisible to a user who knows the exact phrase they want.- Find-in-page is a primary accessibility tool. Keyboard users, screen-reader users, users with cognitive disabilities, and anyone skimming a long document rely on it to locate content directly.
- Search engines and AI crawlers vary in how they treat content hidden with
display: none
.hidden="until-found"
keeps the text in the DOM and reachable, which is the honest signal: this is real content, just collapsed by default.
How to implement #
Mark each collapsed panel and listen for beforematch
so your widget state stays in sync:
<button aria-expanded="false" aria-controls="panel-1">Shipping</button>
<div id="panel-1" hidden="until-found">
We ship worldwide within 48 hours…
</div>
js
const panel = document.getElementById('panel-1');
panel.addEventListener('beforematch', () => {
const button = document.querySelector('[aria-controls="panel-1"]');
button.setAttribute('aria-expanded', 'true');
// remove any matching collapsed class on the button or panel
});
Prefer <details>/<summary> where you can. For the everyday “click a heading to expand a panel” pattern, the native disclosure element gives you focus management, keyboard handling, and find-in-page reachability with zero JavaScript. Reach for
hidden="until-found"
when you need a custom widget that <details>
cannot model — a search-driven FAQ, a complex tab strip, an off-screen mega-menu that must still be findable.## Common mistakes
- Using
display: none
on accordion panels and then wondering why users cannot find the content they remember reading. Switch the closed state tohidden="until-found"
. - Substituting
content-visibility: hidden
in author CSS as a “CSS equivalent” of the attribute. It is not — author-appliedcontent-visibility: hidden
hides content from find-in-page and screen readers in exactly the way the attribute is meant to avoid. - Using
hidden="until-found"
for content that should be permanently hidden — error messages, off-screen utility nodes, suppressed admin tools. Use plainhidden
ordisplay: none
instead. - Forgetting to update
aria-expanded
and the visual chevron state in thebeforematch
handler. The panel opens but the button still claims it is collapsed. - Treating it as a layout primitive. The element still participates in DOM order and document outline — it is hidden, not removed.
Verification #
- Open the page in Chrome or Edge. Press Ctrl/Cmd+F and search for a phrase that lives inside a collapsed panel. The browser should auto-scroll and reveal it.
- Repeat with a panel that uses
display: none
. The search should fail, confirming the regression you are avoiding. - Tab through the widget with a screen reader (VoiceOver, NVDA, JAWS) and confirm the panel announces correctly once expanded.
- Inspect the element in DevTools after a match — the
hidden
attribute should be gone, thebeforematch
listener should have fired, andaria-expanded
should readtrue
.