Accordions show up everywhere - FAQ sections, product details, settings panels. The reflex is to reach for a component library or write a bit of JavaScript to toggle each panel open. You almost never need to. HTML has a built-in element for exactly this, and it’s been quietly supported in every browser for years.
Here’s one built from nothing but <details> and a little CSS. Click a row:
Do you work with clients outside Montenegro?
How long does a typical project take?
Do I own the site when it's done?
It opens, it closes, it remembers which panels are open. None of that cost a line of script.
The markup
The whole thing is two elements. <details> is the panel, <summary> is the part that’s always visible and does the toggling:
<details>
<summary>Do you work with clients outside Montenegro?</summary>
<div>Yes. Most of our work happens over email and calls.</div>
</details>
Click the summary and the browser shows or hides everything after it. Add an open attribute and it starts expanded. Stack a few of these and you have an accordion.
Why this is worth doing
You’re not just saving yourself some JavaScript. The browser gives you a pile of behavior for free:
- It’s keyboard-operable out of the box - tab to a summary, hit Enter or Space, it toggles.
- Screen readers announce it correctly as an expandable group, with no
aria-expandedfor you to wire up and keep in sync. - Browser find-in-page can open a closed panel to reveal a match inside it.
Recreating all of that by hand with <div>s and click handlers is exactly the kind of work people quietly get wrong.
Styling the marker
The one rough edge is the default triangle, which looks different in every browser. Hide it and add your own:
summary {
list-style: none; /* removes the marker in Firefox */
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
}
summary::-webkit-details-marker {
display: none; /* removes it in Safari and Chrome */
}
summary::after {
content: "+";
transition: transform 0.2s ease;
}
details[open] summary::after {
transform: rotate(45deg); /* the + turns into an x when open */
}
The trick in that last rule is the details[open] selector. The browser adds and removes the open attribute as the panel toggles, so you can style the open state purely in CSS. Here we rotate a + by 45 degrees so it reads as a close icon - one character, no icon font, no swap.
Animating the open and close
This is the part people reach for JavaScript to fix, because <details> snaps open instantly by default and animating to height: auto was impossible in CSS for years. It isn’t anymore. Two newer features handle it:
:root {
interpolate-size: allow-keywords; /* lets height: auto animate */
}
::details-content {
height: 0;
overflow: hidden;
transition: height 0.3s ease;
}
details[open]::details-content {
height: auto;
}
::details-content targets everything inside the panel except the summary, and interpolate-size is what finally lets a transition run to auto. Treat this as a progressive enhancement: browsers that don’t support it yet just open instantly, which was the old behavior anyway and is perfectly fine.
When to reach for it
Any time the pattern is “click a label, reveal some content” - FAQs, spec tables, filter groups, disclosure widgets - start here and only add JavaScript if you hit something the element genuinely can’t do, like having one panel close another automatically. For a plain FAQ section it’s hard to justify anything more. Leaning on what the browser already gives you is a big part of how you keep a site fast and accessible.



