An ongoing series on techniques that ship with the browser already - no library, no build step required.
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?
HTML
<div class="acc-demo">
<details>
<summary>Do you work with clients outside Montenegro?</summary>
<div class="acc-body">Yes. Most of our work happens over email and calls, so where you are rarely matters.</div>
</details>
<details>
<summary>How long does a typical project take?</summary>
<div class="acc-body">Most sites land in three to six weeks, depending on scope and how quickly feedback comes back.</div>
</details>
<details>
<summary>Do I own the site when it's done?</summary>
<div class="acc-body">Completely. The code, the domain, the accounts - all yours to keep.</div>
</details>
</div>CSS
.acc-demo details {
border: 1px solid var(--color-gray-200);
border-radius: 6px;
margin-bottom: 8px;
background: var(--color-white);
}
.acc-demo summary {
cursor: pointer;
padding: 0.85rem 1rem;
font-weight: 600;
list-style: none;
display: flex;
justify-content: space-between;
align-items: center;
color: var(--color-gray-900);
}
.acc-demo summary::-webkit-details-marker {
display: none;
}
.acc-demo summary::after {
content: "+";
font-size: 1.25rem;
line-height: 1;
color: var(--accent);
transition: transform 0.2s ease;
}
.acc-demo details[open] summary::after {
transform: rotate(45deg);
}
.acc-demo .acc-body {
padding: 0 1rem 1rem;
color: var(--color-gray-500);
}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.
Worth checking that claim rather than just asserting it, because the two features aren’t at the same stage:
Browser support · ::details-content
Support data from MDN’s browser-compat-data project (v8.0.4), sourced from Mozilla. Last checked 2026-07-03.
Browser support · interpolate-size
Support data from MDN’s browser-compat-data project (v8.0.4), sourced from Mozilla. Last checked 2026-07-03.
::details-content is in every current engine. interpolate-size is not - right now it’s a Chrome/Edge-only trick, with Firefox and Safari both tracking it but not shipping it yet. That’s a bigger gap than “progressive enhancement” sometimes implies: most Firefox and Safari visitors will get the plain instant-open behavior, not a degraded version of the animation. Still the right way to write it - nobody sees anything broken, and the day the other two engines catch up, the animation just starts working with no code changes on your end.
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.



