How to Create a CSS-Only Accordion Without JavaScript (Details and Summary Tags)

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:

  1. 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.
  2. The checkbox or radio hack (legacy): hidden <input> elements paired with <label> headers and the :checked selector. 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
faq accordion

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 open to 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;
}
faq accordion

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 open attribute 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.

faq accordion

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-keywords lets the browser interpolate to and from auto.
  • ::details-content targets the hidden content wrapper the browser creates inside <details>.
  • transition-behavior: allow-discrete keeps 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.

faq accordion

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:

  1. Do not add ARIA. No role="button", no aria-expanded on the summary. The browser already exposes the correct state, and manual ARIA usually overrides it incorrectly.
  2. Keep a visible focus indicator on <summary>.
  3. Keep headers short. A summary is a control, not a paragraph.
  4. 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.
  5. Respect prefers-reduced-motion and disable the height transition.
  6. Test contrast on the header background, hover state and the chevron colour.
  7. 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.
faq accordion

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: flex or grid on <summary> in old Safari without removing the webkit marker first, which can hide the click target.
  • Multiple open attributes 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.

Recent Posts

No Posts Found!

Categories

Tags

    Subscribe

    You have been successfully Subscribed! Ops! Something went wrong, please try again.

    About Us

    Express Jam Studio was founded in 2004 by John Smith. John had previously worked for a courier company, but he saw an opportunity to start his own business in the web design and development industry.

    Contact Info

    Copyright © 2022 Express Jam Studio. All Rights Reserved.