The basic disclosure widget is well known; what's less well known is that HTML now natively covers the entire accordion pattern ā exclusive groups, state-change events, and real height-animatable open/close transitions ā that used to require a JS component.
1Exclusive Accordions With The name Attribute
Giving several <details> elements the same name attribute value groups them into a mutually exclusive set ā the browser guarantees that opening one automatically closes any others sharing that name, entirely as native behavior with zero JavaScript coordination. This directly replaces the previously-standard pattern of a shared JS array or object tracking which panel is currently open and manually toggling the rest closed on each click.
A page can have multiple independent named groups simultaneously ā a FAQ accordion using name="faq" and an unrelated settings panel using name="settings" don't interact with each other at all, since grouping is scoped purely by matching name values, not by DOM proximity.
2Reacting To State Changes: toggle And beforetoggle
The toggle event fires on a <details> element after its open state has changed, carrying newState and oldState properties reporting "open" or "closed" directly ā a cleaner API than manually reading the .open boolean inside a generic event handler. The newer beforetoggle event fires just before the change and is cancelable, letting code prevent a state transition under specific conditions (for instance, blocking a panel from closing while an async operation inside it is still pending).
A common, genuinely useful pattern: listening for toggle and checking newState === "open" to lazy-load a panel's content only the first time a user actually expands it, rather than fetching or rendering content for every accordion item up front regardless of whether it's ever opened.
3Restyling The Marker And Animating Real Height Transitions
The default disclosure triangle is the <summary> element's list marker, fully accessible through the standard ::marker pseudo-element ā content can swap its glyph entirely (a +/ā pair, or a rotating chevron via a transform on a ::before alternative), matching exactly how markers are styled on ordinary list items.
Animating the actual expand/collapse *height* ā not just a fade ā has historically been the genuinely hard part, since height: auto isn't natively transitionable. The interpolate-size: allow-keywords CSS property changes that, enabling transitions between auto and a fixed height, finally making a smooth, JS-free accordion height animation achievable in pure CSS.
4Free Disclosure Semantics ā And The One Real Pitfall To Avoid
<summary> behaves as a fully accessible disclosure button by default: keyboard-focusable, activatable with Enter or Space, and announced by assistive technology with an implicit expanded/collapsed state that updates automatically as <details> opens and closes ā none of it requires manual role="button" or aria-expanded bookkeeping.
The one genuine trap is nesting another interactive element ā a <button> or <a> ā directly inside <summary>. Because <summary> is already an interactive control, this creates nested interactive controls, which is invalid HTML with inconsistent, browser- and AT-dependent behavior (some browsers may not correctly dispatch clicks to the inner control at all). If a summary genuinely needs an additional clickable action beyond toggling, that control belongs as a sibling inside the <details> body, not nested inside <summary> itself.
5Step-by-Step Breakdown
A Native Accordion, No JavaScript Required. A FAQ accordion where opening one question closes the others used to require JavaScript state management. The name attribute on <details> makes that grouping behavior entirely native ā one more case of the platform absorbing what used to be a component library's job.
The name Attribute Creates Exclusive Accordion Groups. Giving multiple <details> elements the same name attribute value makes them mutually exclusive ā opening one automatically closes the others in the group, exactly the accordion behavior developers previously built by hand with a shared JS state object and manual open/close toggling.
The name Attribute. What happens when two <details> elements share the same name attribute value?
- āThey become mutually exclusive ā opening one closes the others in the group
- āNothing functional; it's purely a CSS styling hook
- āThey open and close together, in sync
toggle And beforetoggle Events Carry Old/New State. The toggle event fires after a <details> element's open state changes, and the newer, cancelable beforetoggle event fires just before ā both carry newState and oldState properties ("open"/"closed"), letting you react to or intercept state changes, such as lazy-loading content only when a panel is first opened.
The toggle Event. What does the toggle event's newState property tell you?
- āWhether the details element is now "open" or "closed"
- āA boolean for whether the element is currently visible in the viewport
- āThe current progress of an open/close CSS animation
Styling ::marker And Animating Open/Close Height. The default disclosure triangle is the <summary> element's ::marker pseudo-element, fully restylable or removable (list-style: none, or ::marker { content: '' }); animating the actual open/close HEIGHT transition (rather than just opacity) has historically been the hard part, now addressed by the interpolate-size CSS property enabling auto-to-fixed height transitions.
Animating <details> Open/Close. What CSS pseudo-element controls the default disclosure triangle on <summary>?
- ā::marker
- ā::before, requiring content: "" to be set manually first
- āThere's no styling hook; it's a fixed browser image
Accessibility: Built-In Disclosure Semantics, With One Real Pitfall. <summary> automatically behaves like a button (keyboard-activatable with Enter/Space, focusable, and announced with an implicit expanded/collapsed state) with zero ARIA needed ā but placing another genuinely interactive element (a link, a button) inside <summary> creates nested-interactive-control markup, which is invalid and produces unpredictable, inconsistent behavior across browsers and assistive technology.
The Nested-Control Pitfall. Why is placing a <button> or <a> directly inside a <summary> element problematic?
- āIt creates nested interactive controls, which is invalid markup with unpredictable cross-browser/AT behavior
- āIt's purely a visual/CSS styling inconvenience with no functional impact
- āIt isn't actually a problem at all
Programmatic Control And When To Reach For A Custom Accordion Instead. The .open boolean property lets JS read or set state directly (details.open = true), useful for 'expand all' controls across a group without a shared name; but for accordions needing animated height beyond what interpolate-size covers cleanly in every target browser, or requiring more complex multi-panel state than exclusive/independent, a custom-built component may still be the more predictable choice.
Programmatic Control. How do you programmatically open every <details> element in a group at once from JavaScript, overriding the exclusive name behavior?
- āSet the .open property to true directly on each element
- āIt's impossible to override the exclusive name grouping from JS
- āThe name attribute must be permanently removed first
Advanced <details>/<summary> Mastered. You now know how to build exclusive accordion groups with the name attribute, react to state changes via the toggle/beforetoggle events, restyle and animate the disclosure widget, and avoid the one genuine accessibility pitfall ā nesting interactive controls inside <summary>.
Open A Disclosure Widget By Default. Add the open attribute so the <details> starts expanded instead of collapsed.
Level Up š
Advanced cheat sheets, SEO tricks, and interview prep for this topic.
Browser Support
Fully supported.
Fully supported.
Fully supported.
Fully supported.
Accessibility (A11y)
1<summary> Announces Expanded/Collapsed State Automatically ā No ARIA Required
The implicit disclosure semantics mean screen reader users get correct state announcements without any manual aria-expanded management, as long as native <details>/<summary> is used rather than a custom-built div-based imitation.
2Never Nest A <button> Or <a> Directly Inside <summary>
This creates invalid nested interactive controls with unpredictable cross-browser and cross-AT behavior ā place any additional action as a sibling within the <details> body instead.
SEO Implications
- 1
Content Inside A Collapsed <details> Is Present In The DOM And Indexable
Search engines can access and index content inside a closed <details> element since it's genuinely part of the rendered DOM, not injected only on interaction ā useful for FAQ-style content that benefits from being indexed while staying visually collapsed by default.
Best Practices
Use A Shared name Attribute For Any Accordion Where Only One Panel Should Be Open At A Time
This replaces manual JS state coordination entirely, and is less error-prone than a hand-rolled 'close all others' click handler.
Reach For interpolate-size Or A Measured-Height Transition Before Reaching For A JS Animation Library
Native CSS height animation covers the overwhelming majority of accordion animation needs without adding a dependency.
Frequent Bugs
Multiple accordion panels can be open simultaneously despite the design intending only one at a time.
Give all <details> elements in that group the same name attribute value, letting the browser enforce exclusivity natively.
A button inside <summary> doesn't reliably respond to clicks/keyboard activation across browsers.
Move the button out as a sibling inside the <details> body rather than nesting it inside <summary>, which is already an interactive control.
Real-World Examples
A FAQ Accordion With Lazy-Loaded Content
An exclusive FAQ accordion that only fetches a panel's detailed content the first time it's actually opened.
<details name="faq" data-id="shipping">
<summary>Shipping options?</summary>
<div class="panel"></div>
</details>
<script>
document.querySelectorAll("details[name=faq]").forEach(d => {
d.addEventListener("toggle", async (e) => {
if (e.newState === "open" && !d.dataset.loaded) {
d.querySelector(".panel").textContent = await fetchFaqContent(d.dataset.id);
d.dataset.loaded = "true";
}
});
});
</script>