A CSS only accordion is one of the easiest performance wins available on a modern site. No JavaScript bundle, no hydration delay, no ARIA wiring to maintain, and keyboard support that works the second the HTML is parsed. The native <details> and <summary> elements do the heavy lifting, and modern CSS now handles the part that used to require scripting: a smooth open and close animation from zero to auto height.
This tutorial gives you the full recipe, plus two copy-ready blocks you can paste straight into a page today: an FAQ accordion and a product specs accordion. We also cover the parts most tutorials skip, such as what actually happens in browsers that do not support the newest animation properties, and when the old checkbox hack is still the right call.
Quick answer: what is a CSS-only accordion?
A CSS only accordion is an expand and collapse UI built with HTML and CSS alone, with no JavaScript event listeners. There are two ways to build one:
- The native way (recommended): a series of
<details>elements, each with a<summary>header. The browser handles open and close state, focus, keyboard, and screen reader semantics. - The checkbox or radio hack (legacy): hidden
<input>elements paired with<label>headers and the:checkedselector. It animates well but it lies to assistive technology, because a checkbox is not a disclosure widget.
Unless you have a very specific reason, use <details>. Here is how the two compare.
| Criteria | details / summary | Checkbox hack |
|---|---|---|
| Keyboard support | Built in (Tab, Enter, Space) | Needs tabindex tricks |
| Screen reader semantics | Announced as an expandable group | Announced as a checkbox |
| Find in page (Ctrl+F) | Opens the matching panel | Fails on hidden content |
| One panel open at a time | Yes, with the name attribute |
Yes, with radio inputs |
| Close animation | Yes in modern browsers | Yes everywhere |
| Markup weight | Minimal | Extra inputs and labels |

Step 1: the markup
Start with clean, semantic HTML. Each accordion item is one <details> element, the header is the <summary>, and everything after it is the panel.
<div class="accordion">
<details class="accordion__item" open>
<summary class="accordion__header">What is included in the free plan?</summary>
<div class="accordion__panel">
<p>Unlimited projects, 2 GB of storage and community support.</p>
</div>
</details>
<details class="accordion__item">
<summary class="accordion__header">Can I cancel at any time?</summary>
<div class="accordion__panel">
<p>Yes. Cancel in one click from your dashboard, no phone call needed.</p>
</div>
</details>
<details class="accordion__item">
<summary class="accordion__header">Do you offer invoices for companies?</summary>
<div class="accordion__panel">
<p>Every payment generates a PDF invoice with your VAT number.</p>
</div>
</details>
</div>
Three rules to remember:
- The
<summary>must be the first child of<details>, otherwise the browser generates an implicit one. - Wrap the panel content in a single element. That wrapper is what you will pad and animate.
- Add
opento the item you want expanded on load. Opening the first item is good for engagement, but leave all closed if the list is long.
Step 2: remove the default marker
Browsers draw a small triangle before the summary text. Remove it so you can design your own icon.
.accordion__header {
list-style: none; /* Chrome, Firefox, Safari 18+ */
}
.accordion__header::-webkit-details-marker {
display: none; /* older Safari */
}
If you like the native triangle and just want it recoloured or resized, keep it and style the ::marker pseudo element instead:
.accordion__header::marker {
color: #2563eb;
font-size: 1.1em;
}

Step 3: base styles
.accordion {
--acc-border: #e5e7eb;
--acc-accent: #2563eb;
--acc-hover: #f8fafc;
max-width: 44rem;
border: 1px solid var(--acc-border);
border-radius: 12px;
overflow: hidden;
background: #fff;
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
}
.accordion__item + .accordion__item {
border-top: 1px solid var(--acc-border);
}
.accordion__header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 1rem 1.25rem;
font-weight: 600;
line-height: 1.4;
cursor: pointer;
user-select: none;
list-style: none;
}
.accordion__header:hover {
background: var(--acc-hover);
}
.accordion__header:focus-visible {
outline: 3px solid var(--acc-accent);
outline-offset: -3px;
}
.accordion__panel {
padding: 0 1.25rem 1.25rem;
color: #475569;
line-height: 1.65;
}
Note the :focus-visible rule. Never delete the focus ring on a summary, it is the only visual cue a keyboard user gets.
Step 4: a custom marker that rotates
A chevron built from two borders costs zero extra requests and inherits the text colour.
.accordion__header::after {
content: "";
flex: none;
width: 0.6rem;
height: 0.6rem;
margin-inline-end: 0.2rem;
border-right: 2px solid currentColor;
border-bottom: 2px solid currentColor;
transform: rotate(45deg) translateY(-2px);
transition: transform 220ms ease;
}
.accordion__item[open] > .accordion__header::after {
transform: rotate(-135deg) translateY(-2px);
}
Prefer a plus and minus sign? Swap the pseudo element content:
.accordion__header::after { content: "+"; font-size: 1.4rem; line-height: 1; border: 0; transform: none; }
.accordion__item[open] > .accordion__header::after { content: "\2212"; }
Step 5: make it an exclusive accordion (one panel at a time)
This used to be the number one reason people reached for JavaScript. It is now a single HTML attribute. Give every <details> in the group the same name value and the browser closes the others automatically.
<details class="accordion__item" name="faq"> ... </details>
<details class="accordion__item" name="faq"> ... </details>
<details class="accordion__item" name="faq"> ... </details>
Two details worth knowing:
- Only one item in a named group may carry the
openattribute in the source, otherwise the last one wins. - Grouped items do not have to be siblings, although keeping them together is far easier to reason about.
Leave name off if you want visitors to open several sections side by side, which is usually better for comparison-style content such as product specs.

Step 6: animate the open state with pure CSS
Animating from height 0 to auto was impossible for years. Three modern features solve it together:
interpolate-size: allow-keywordslets the browser interpolate to and fromauto.::details-contenttargets the hidden content wrapper the browser creates inside<details>.transition-behavior: allow-discretekeeps the content visible while the closing transition plays.
:root {
interpolate-size: allow-keywords;
}
.accordion__item::details-content {
block-size: 0;
overflow: hidden;
opacity: 0;
transition:
block-size 280ms ease,
opacity 220ms ease,
content-visibility 280ms allow-discrete;
}
.accordion__item[open]::details-content {
block-size: auto;
opacity: 1;
}
@media (prefers-reduced-motion: reduce) {
.accordion__item::details-content,
.accordion__header::after {
transition: none;
}
}
That is the whole animation. No scrollHeight measurement, no requestAnimationFrame, no layout thrashing.
What happens in browsers without ::details-content
Nothing breaks. The accordion still opens and closes instantly, exactly like a plain <details> element. That is textbook progressive enhancement: the feature degrades to a snap, not to a broken widget. If you want to be explicit about it, wrap the animation in a feature query:
@supports selector(::details-content) {
/* animation rules here */
}
The grid 0fr to 1fr fallback
You will see the grid-template-rows: 0fr trick recommended everywhere. It is excellent, but be aware of its limit with <details>: the browser hides the content the instant the element closes, so you get an opening animation and an abrupt close. It shines with the checkbox hack, where the content is never structurally hidden. For the wider picture, see 30+ CSS Accordions: Responsive Collapsible Code Snippets.
/* Checkbox based accordion, full open and close animation everywhere */
.acc-input { position: absolute; opacity: 0; width: 1px; height: 1px; }
.acc-body { display: grid; grid-template-rows: 0fr; transition: grid-template-rows 300ms ease; }
.acc-input:checked ~ .acc-body { grid-template-rows: 1fr; }
.acc-body > div { overflow: hidden; }
Our recommendation: use the native version. Accessibility and find-in-page support beat a perfectly smooth closing curve on older browsers.
Copy-ready block 1: FAQ accordion
Paste this into any page. It is self contained, exclusive, animated and accessible.
<section class="faq" aria-labelledby="faq-title">
<h2 id="faq-title">Frequently asked questions</h2>
<div class="accordion">
<details class="accordion__item" name="faq" open>
<summary class="accordion__header">How long does deployment take?</summary>
<div class="accordion__panel"><p>Under two minutes for a standard Express app.</p></div>
</details>
<details class="accordion__item" name="faq">
<summary class="accordion__header">Is there a bandwidth limit?</summary>
<div class="accordion__panel"><p>Fair use applies, with 1 TB included on every paid plan.</p></div>
</details>
<details class="accordion__item" name="faq">
<summary class="accordion__header">Can I migrate an existing project?</summary>
<div class="accordion__panel"><p>Yes, point your repository at us and we detect the build command.</p></div>
</details>
</div>
</section>
<style>
:root { interpolate-size: allow-keywords; }
.accordion { max-width: 44rem; border: 1px solid #e5e7eb; border-radius: 12px; overflow: hidden; background: #fff; }
.accordion__item + .accordion__item { border-top: 1px solid #e5e7eb; }
.accordion__header { display: flex; align-items: center; justify-content: space-between; gap: 1rem; padding: 1rem 1.25rem; font-weight: 600; cursor: pointer; list-style: none; }
.accordion__header::-webkit-details-marker { display: none; }
.accordion__header:hover { background: #f8fafc; }
.accordion__header:focus-visible { outline: 3px solid #2563eb; outline-offset: -3px; }
.accordion__header::after { content: ""; flex: none; width: .6rem; height: .6rem; border-right: 2px solid currentColor; border-bottom: 2px solid currentColor; transform: rotate(45deg); transition: transform 220ms ease; }
.accordion__item[open] > .accordion__header::after { transform: rotate(-135deg); }
.accordion__panel { padding: 0 1.25rem 1.25rem; color: #475569; line-height: 1.65; }
.accordion__item::details-content { block-size: 0; overflow: hidden; opacity: 0; transition: block-size 280ms ease, opacity 220ms ease, content-visibility 280ms allow-discrete; }
.accordion__item[open]::details-content { block-size: auto; opacity: 1; }
@media (prefers-reduced-motion: reduce) { .accordion__item::details-content, .accordion__header::after { transition: none; } }
</style>
SEO note: keep FAQ answers in the HTML, not injected by script, so crawlers always read them. Collapsed content inside <details> is indexed normally. You can still add FAQPage structured data for clarity, but do not count on rich results, since Google now restricts that feature to a narrow set of authoritative sites.

Copy-ready block 2: product specs accordion
For specification sheets, drop the name attribute so several sections can stay open while a visitor compares values.
<div class="accordion">
<details class="accordion__item" open>
<summary class="accordion__header">Technical specifications</summary>
<div class="accordion__panel">
<table style="border-collapse:collapse;width:100%" border="1">
<tr><th style="border:1px solid #ccc;padding:8px;text-align:left">CPU</th><td style="border:1px solid #ccc;padding:8px">4 vCPU</td></tr>
<tr><th style="border:1px solid #ccc;padding:8px;text-align:left">RAM</th><td style="border:1px solid #ccc;padding:8px">8 GB</td></tr>
<tr><th style="border:1px solid #ccc;padding:8px;text-align:left">Storage</th><td style="border:1px solid #ccc;padding:8px">160 GB NVMe</td></tr>
</table>
</div>
</details>
<details class="accordion__item">
<summary class="accordion__header">Shipping and delivery</summary>
<div class="accordion__panel"><p>Instant provisioning, no physical shipping.</p></div>
</details>
<details class="accordion__item">
<summary class="accordion__header">Warranty and support</summary>
<div class="accordion__panel"><p>24/7 support with a 99.95% uptime SLA.</p></div>
</details>
</div>
Accessibility checklist
The native element gives you most of this for free, but verify each point before shipping:
- Do not add ARIA. No
role="button", noaria-expandedon the summary. The browser already exposes the correct state, and manual ARIA usually overrides it incorrectly. - Keep a visible focus indicator on
<summary>. - Keep headers short. A summary is a control, not a paragraph.
- Heading structure: if your design needs a heading, nest it inside the summary (
<summary><h3>Title</h3></summary>) rather than wrapping the details in one. - Respect
prefers-reduced-motionand disable the height transition. - Test contrast on the header background, hover state and the chevron colour.
- Test Ctrl+F. Text inside a closed
<details>should be found and the panel should open automatically.
Why a CSS only accordion is faster
- Zero JavaScript for the component: nothing to download, parse, compile or execute.
- No hydration gap: the accordion is interactive as soon as the HTML arrives, which helps Interaction to Next Paint.
- Compositor friendly animations: opacity and transform run off the main thread, and the height interpolation is handled natively.
- No layout shift on load, because there is no script rewriting the DOM after first paint.
- Works with JavaScript disabled or failed, which still happens more often than analytics suggest.

Browser support
| Feature | Chrome / Edge | Safari | Firefox | If unsupported |
|---|---|---|---|---|
| details / summary | Yes | Yes | Yes | n/a |
| name attribute (exclusive) | 120+ | 17.2+ | 130+ | Multiple panels can stay open |
| ::details-content | 131+ | 18.4+ | 139+ | Instant open and close |
| interpolate-size / calc-size() | 129+ | Recent versions | In progress | Height snaps, opacity still fades |
| transition-behavior: allow-discrete | 117+ | 17.4+ | 129+ | Close animation is skipped |
Support for the animation properties is moving quickly, so check caniuse.com for the current numbers before you ship a critical page. In every case the accordion remains fully usable.
Common mistakes to avoid
- Adding padding directly on
::details-content. Padding is not part of the animated block size and causes a jump. Put padding on your inner panel wrapper instead. - Setting
display: flexorgridon<summary>in old Safari without removing the webkit marker first, which can hide the click target. - Multiple
openattributes inside one named group. - Hiding important content such as pricing or shipping fees behind a collapsed panel by default.
- Removing the outline instead of restyling it.
- Using the checkbox hack for an FAQ that needs to be crawled and searched in page.
FAQ
Can you really build an accordion with only CSS?
Yes. With <details> and <summary> the open and close behaviour is native HTML, so CSS only handles the visuals and the animation. No JavaScript is involved at any point.
How do I make only one section open at a time without JavaScript?
Give every <details> in the group the same name attribute, for example name="faq". The browser closes the currently open item when another one is expanded. Anyone digging further should read Create an Accessible Accordion Using HTML and CSS.
How do I animate a details element from height 0 to auto?
Set interpolate-size: allow-keywords on :root, then transition block-size on ::details-content from 0 to auto, adding content-visibility with allow-discrete so the closing animation can play out.
Is a CSS only accordion accessible?
The native version is accessible by default. It is focusable, operable with Enter and Space, exposed as an expandable disclosure to screen readers, and searchable with find in page. Keep the focus ring visible and avoid adding redundant ARIA attributes.
Does hiding content in an accordion hurt SEO?
No. Content inside a collapsed <details> element is present in the HTML and indexed normally. Problems only arise when content is injected by JavaScript after interaction.
Should I still use the checkbox accordion?
Only when you need an identical open and close animation on very old browsers, or a purely decorative widget. For anything users rely on, the native element is the better engineering choice.
Can I style the little triangle marker?
Yes. Use summary::marker to recolour or resize it, or remove it with list-style: none plus ::-webkit-details-marker { display: none } and draw your own icon with a ::after pseudo element.
Wrapping up
A css only accordion built on <details> and <summary> is smaller, faster and more accessible than any scripted alternative, and in 2026 it finally animates smoothly with nothing but CSS. Copy the FAQ block above, adjust the custom properties to match your brand, and delete the accordion script from your bundle. codepen.io published something useful on the subject.
