The HTML5 Accordion You Already Have: details and summary in Practice
The details and summary elements give you a working accordion with no JavaScript, real keyboard support, and an inspectable open attribute. Here's how to style it — and where it stops.
17 Apr 2026, 04:14 UTC

Start with the markup, not the JavaScript
Most FAQ sections begin as a div, a button, and forty lines of script that reimplement focus management, ARIA state, and click handling. HTML5 already ships that widget. <details> is the container, and its first <summary> child is the clickable label. The browser owns the toggle, the focus ring, and the expanded/collapsed state.
The argument here is narrow: for FAQ lists, changelogs, and settings panels, native disclosure is usually the better default — and the places it falls short are predictable enough to plan around.
What you get without writing a line of script
- Keyboard support. The summary is focusable. Enter and Space toggle the panel. No
keydownhandler needed. - State you can inspect. Opening a panel adds the boolean
openattribute to the<details>element. That is the entire state model. - An accessibility tree entry. Browsers expose the summary with an expanded or collapsed state, so assistive technology announces it without extra ARIA attributes.
- A
toggleevent. It fires on the<details>element after the state changes — useful for lazy-loading a chart or recording analytics.
Because state lives in an attribute, you can also set it server-side. Rendering <details open> means the first panel is expanded in the initial HTML, with no flash of collapsed content while a script catches up.
A worked example: a grouped FAQ
Run this as a static .html file opened directly in a browser — no build step and no server required. The only permission involved is write access to the file you are editing.
<details class="faq" name="faq">
<summary><h3>Do I need JavaScript for this?</h3></summary>
<p>No. The browser handles toggling, focus, and keyboard activation.</p>
</details>
<details class="faq" name="faq">
<summary><h3>Can I open one panel from another button?</h3></summary>
<p>Yes — set the <code>open</code> property on the element.</p>
</details>
Two details matter. First, the heading inside <summary> is valid markup and gives screen-reader users heading navigation through the FAQ. Second, name="faq" groups the panels into an exclusive accordion, so opening one closes the others — the behavior you would otherwise script by hand. That grouping attribute is newer than the element itself, so confirm it in every browser in your support matrix before depending on it.
Styling the marker is where people get stuck. The default triangle is drawn by the browser and is not a normal CSS box, so you hide it and draw your own:
summary {
cursor: pointer;
list-style: none; /* Firefox */
}
summary::-webkit-details-marker { display: none; } /* older WebKit/Blink */
summary::marker { content: ""; } /* modern browsers */
summary::after {
content: "+";
float: right;
}
details[open] > summary::after { content: "−"; }
You may not need all three marker rules, but keeping them costs nothing and covers older engines. If you would rather not fight the marker at all, leave it alone: the native triangle is consistent within a browser, just not across browsers.
Driving the panel from an external button
If a "Show shipping details" link elsewhere on the page must open a panel, you are changing state from script, so you own the timing:
const panel = document.querySelector('#shipping-details');
document.querySelector('#shipping-toggle').addEventListener('click', () => {
panel.open = !panel.open;
});
Set open in the HTML if the panel should start expanded. Adding it from script on load can produce a visible flash of content before the script runs.
Where native disclosure stops being enough
Animation is the honest limitation. A closed <details> does not render its children, so there is no height to transition — the panel appears and disappears instantly. Newer CSS aimed at exactly this (the ::details-content pseudo-element combined with interpolate-size and transition-behavior: allow-discrete) is landing in some browsers, but treat it as progressive enhancement and verify per target browser rather than assuming smooth animation everywhere.
Other trade-offs worth naming:
- Cross-browser look. Default padding, marker size, and font weight differ. Budget a small reset if visual consistency matters.
- Do not nest interactive controls in
<summary>. The summary is itself the control; a link or button inside it creates ambiguous activation. - Print and indexing. Closed panels may not print, and if you rely on collapsed text being indexed, check with your own crawler or index tooling instead of assuming.
How to confirm it actually works
- Open the page, right-click the summary, and choose Inspect. Click the summary and watch the
openattribute appear and disappear on the<details>element. - In DevTools, open the Accessibility pane for the summary and confirm it is exposed with an expanded or collapsed state.
- Press Tab until the summary has focus, then press Enter, then Space. The panel should toggle both times with no mouse involved.
- Toggle
openmanually in the Elements panel and confirm the content shows and hides immediately — that is the native behavior you are building on. - Repeat steps 1–3 in each browser you support, including any older engine still in scope.
If all five pass, you have replaced a scripted accordion with markup. If step 3 fails, the cause is usually a CSS rule that removed focusability or an overlay intercepting clicks — check pointer-events and stacking order before reaching for JavaScript.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.