Skip to content

COMPONENT

Accordion

A stack of disclosures that animate their own height, and only one open at a time if that is what you want. No script anywhere: the element is <details> and exclusivity is an attribute.

One answer at a time

Open another and the first closes itself. That is the name attribute, not a handler.

Does it need JavaScript?
No. The disclosure is a native element, and only one being open at a time is an attribute rather than a script.
What happens on a browser without the animation?
It opens instantly. The height transition is guarded, so a browser that cannot run it loses the movement and never the content.
Can two be open together?
Yes β€” drop the name attribute. It is what makes a group exclusive, so without it each item answers only for itself.
HTML
<div class="accordion" x-ds-accordion>
  <details class="accordion-item" name="basics" open>
    <summary class="accordion-head">
      Does it need JavaScript?
      <span class="caret" aria-hidden="true">β–Ύ</span>
    </summary>
    <div class="accordion-panel">
      No. The disclosure is a native element, and only one being open at a time
      is an attribute rather than a script.
    </div>
  </details>
  <details class="accordion-item" name="basics">
    <summary class="accordion-head">
      What happens on a browser without the animation?
      <span class="caret" aria-hidden="true">β–Ύ</span>
    </summary>
    <div class="accordion-panel">
      It opens instantly. The height transition is guarded, so a browser that
      cannot run it loses the movement and never the content.
    </div>
  </details>
  <details class="accordion-item" name="basics">
    <summary class="accordion-head">
      Can two be open together?
      <span class="caret" aria-hidden="true">β–Ύ</span>
    </summary>
    <div class="accordion-panel">
      Yes β€” drop the <code>name</code> attribute. It is what makes a group
      exclusive, so without it each item answers only for itself.
    </div>
  </details>
</div>

Boxed

Separate cards instead of a ruled stack, for a page that already has hairlines running through it.

Shipping
Two working days, tracked, anywhere.
Returns
Thirty days, no reason required.
HTML
<div class="accordion accordion-boxed">
  <details class="accordion-item">
    <summary class="accordion-head">
      Shipping
      <span class="caret" aria-hidden="true">β–Ύ</span>
    </summary>
    <div class="accordion-panel">Two working days, tracked, anywhere.</div>
  </details>
  <details class="accordion-item">
    <summary class="accordion-head">
      Returns
      <span class="caret" aria-hidden="true">β–Ύ</span>
    </summary>
    <div class="accordion-panel">Thirty days, no reason required.</div>
  </details>
</div>

Prose

Body size and body colour in the panel, for answers that are paragraphs rather than lines.

Why a disclosure rather than a tab

A tab set says these things are alternatives and you want one of them. A disclosure says this is a long page and you may not want all of it. The difference shows on a phone, where tabs run out of room and an accordion simply gets taller.

When to reach for something else

A panel is clipped while it animates, so anything that escapes its own box β€” a menu, a tooltip β€” is cut off inside one.

HTML
<div class="accordion accordion-prose">
  <details class="accordion-item" open>
    <summary class="accordion-head">
      Why a disclosure rather than a tab
      <span class="caret" aria-hidden="true">β–Ύ</span>
    </summary>
    <div class="accordion-panel">
      <p>
        A tab set says these things are alternatives and you want one of them. A
        disclosure says this is a long page and you may not want all of it. The
        difference shows on a phone, where tabs run out of room and an accordion
        simply gets taller.
      </p>
    </div>
  </details>
  <details class="accordion-item">
    <summary class="accordion-head">
      When to reach for something else
      <span class="caret" aria-hidden="true">β–Ύ</span>
    </summary>
    <div class="accordion-panel">
      <p>
        A panel is clipped while it animates, so anything that escapes its own
        box β€” a menu, a tooltip β€” is cut off inside one.
      </p>
    </div>
  </details>
</div>

One open, or many

Give every item in a group the same name and the browser closes the open one for you. Leave it off and each item answers only for itself. This is a real HTML feature rather than a convention, so it needs nothing from this library and nothing from a script.

<details class="accordion-item" name="faq">   <!-- one open at a time -->
<details class="accordion-item">              <!-- answers only for itself -->

Reference

Read from components/accordion.css when this page was built, so it cannot fall behind the stylesheet.

CLASSES.accordion.accordion-item.accordion-head.caret.accordion-panel.accordion-boxed.accordion-prose
SETTINGSSet the first on one element; a theme sets the second for all of them.--accordion-pad β€” falls back to --ds-pad-accordion--accordion-motion β€” falls back to --ds-motion-panel
READS--ds-accent--ds-focus-ring-inset--ds-focus-width--ds-font-body--ds-font-display--ds-gap-inline--ds-gap-tight--ds-hairline--ds-ink--ds-ink-read--ds-ink-soft--ds-motion-fast--ds-radius-control--ds-raised--ds-rule--ds-sunk--ds-text-body--ds-text-small--ds-text-ui--ds-track-subtle

Notes

WHY THE HEIGHT IS A GRID ROW AND NOT A HEIGHT

height: 0 to auto under interpolate-size: allow-keywords is the tidier spelling, and it is Chromium-only. Filmed side by side at the same duration, Firefox went from 23px to 156px in a single frame while Chromium eased across nine. A fraction unit needs no keyword interpolation, so a grid row growing from zero animates in both β€” which is why the panel is a grid row.

AND THE COLLAPSED ROW IS minmax(0, 0fr), NEVER A BARE 0fr

This one arrived as a bug report: jittery in Chrome, smooth in Firefox, and the elements below the accordion jumping twice on every toggle. Here is what it turned out to be, and you will hit it too if you write this yourself.

A bare 0fr track means minmax(auto, 0fr), and that auto minimum is the item's minimum contribution β€” which counts its padding. So the row could never go below the panel's own padding-block-end. While the panel is shut you cannot see it, because content-visibility: hidden contains the size away to nothing. The moment that containment lifts, the floor applies.

Measured in Chromium 151, the panel's first three painted heights were 16px, 16px, 16px β€” the padding, exactly. So an open began with a 16px jump, and a close stalled at 16px and then snapped to zero, with the eased travel sandwiched between them. Two hard steps a toggle. Firefox 153 never applied the floor at all, which is why the report named one browser. Give the track a definite 0 minimum and the same three frames are 2.05px, 6.28px and 12.11px: the first step of the travel goes from 26% of it to 1.5%.

min-block-size: 0 on the panel is the obvious fix and it does nothing β€” measured, still 16px, 16px, 16px. Padding is in a grid item's minimum contribution whatever its minimum size says, so the floor has to come off the track.

THE ANIMATION IS ON A PSEUDO-ELEMENT, AND THAT IS AN ACCESSIBILITY DECISION

The grid goes on ::details-content. Putting it on a wrapper of your own instead is the version most articles show, and it needs content-visibility: visible on that pseudo to stop the browser hiding the panel before the animation can run.

Read out of the accessibility tree, that is a closed accordion announcing its contents. Measured both ways: on the pseudo, a shut panel is absent from the tree; on a wrapper, its text is in the tree while the panel looks closed. An animation is not worth a disclosure that does not disclose.

THE PANEL IS CLIPPED

Every height animation of this shape clips its content, so a control that escapes its own box β€” a dropdown, a tooltip β€” is cut off inside a panel. That is the cost, and it is the same one every accordion on the web pays. A panel that must overflow wants a modal.

THE CLIPPING GOES ON THE BOX THAT COLLAPSES β€” AND ONLY MAY BECAUSE OF THE LINE ABOVE

overflow: hidden sits on ::details-content, the pseudo-element being collapsed. It lived on the panel for a long time for a reason that was true at the time: with a bare 0fr track, clipping the container stopped the animation dead while looking entirely correct β€” the transition computed, the behaviour was right, and the height moved 52px to 113px in one frame. That was the auto minimum again. A scroll container will not compress below its contents, and its contents were the floor.

A definite 0 removes that, so the container compresses and the animation is untouched: fourteen distinct heights over the same travel, both directions. And it has to be here, because a panel that clips itself still paints outside the box being collapsed β€” at a row half a pixel tall the panel's own box is still sixteen, so its first line of text was drawn straight over the heading beneath it for the opening frames of every toggle. Clipping at the container is what makes a shut panel actually nothing.

ARROW KEYS ARE THE ONE THING IT LACKS

Tab walks into an open panel and through everything in it before reaching the next heading, which is a long way round a column of questions. Add x-ds-accordion to the wrapper and ↑, ↓, Home and End move between the headings β€” see JavaScript. It opens nothing and closes nothing: moving focus is not choosing. Remove it and Tab still reaches every heading in order.

WHERE THE ANIMATION DOES NOT RUN

The two rules that animate it sit behind @supports selector(::details-content). A browser without the pseudo-element applies neither, <details> behaves as it always has, and the reader loses the movement rather than the answer. Reduced motion is handled globally β€” the base layer flattens every duration, this file states no second spelling of it.