CSS Accordion

Expandable content panels using the <details> and <summary> elements.

Published May 19, 2026 Beginner 8 min read

A CSS accordion collapses a stack of panels down to their headings and expands one when the reader opens it. The <details> and <summary> elements already do the hard part. The browser owns the open state, adds and removes the open attribute itself, and exposes the whole thing to assistive technology as a disclosure widget. CSS only has to remove the default marker, style the two states, and rotate an indicator.

Three versions are on this page. The plain <details> list lets any number of panels sit open at once. Adding the name attribute makes the group exclusive, so opening one closes the others without a line of CSS. The third version drops <details> for a checkbox, which is the only way to animate the panel height on its own, and it costs you the native semantics.

Click a heading to expand

What is Pure CSS?
Pure CSS refers to techniques that achieve interactive UI patterns using only CSS, with no JavaScript required.
Which browsers are supported?
All modern browsers support the features used in these examples. Check caniuse.com for specific properties.
Can I use these in production?
Yes! These are real CSS techniques used in production sites. Always consider accessibility requirements for your use case.

HTML

<details open>
  <summary>What is Pure CSS?</summary>
  <div class="accordion-body">Pure CSS refers to techniques that achieve interactive UI patterns using only CSS, no JavaScript required.</div>
</details>
<details>
  <summary>Which browsers are supported?</summary>
  <div class="accordion-body">All modern browsers support the features used in these examples.</div>
</details>

CSS

details {
  width: 100%;
  max-width: 560px;
  border: 1px solid #2a2a2d;
  border-radius: 8px;
  margin-bottom: .5rem;
  overflow: hidden;
}

summary {
  padding: .9rem 1.1rem;
  font-weight: 600;
  font-size: .95rem;
  cursor: pointer;
  list-style: none;
  display: flex;
  justify-content: space-between;
  align-items: center;
  background: #141415;
  color: #f0f0f0;
  user-select: none;
  transition: background .2s;
}

summary::-webkit-details-marker {
  display: none;
}

summary::after {
  content: "+";
  font-size: 1.2rem;
  font-weight: 400;
  color: #b8ff57;
  transition: transform .25s;
}

details[open] summary {
  background: rgba(184, 255, 87, .1);
}

details[open] summary::after {
  transform: rotate(45deg);
}

.accordion-body {
  padding: 1rem 1.1rem;
  font-size: .9rem;
  color: #88888f;
  background: #141415;
  border-top: 1px solid #2a2a2d;
}

Other ways to build it

One panel open at a time

Give every <details> in the group the same name and the browser closes the open one when another is clicked. The styling is identical to the version above, and the CSS file gains nothing at all: exclusivity is an HTML attribute here, not a selector. Browsers that predate the attribute ignore it, so the group falls back to panels that open independently.

Do the panels close each other?
Yes. All three share name="accordion-faq", so opening this one shut whichever was open before it.
How much CSS does that take?
None. The attribute is handled by the browser, so the stylesheet is the same one the first demo uses.
What happens in an older browser?
The attribute is ignored and every panel opens on its own. Nothing breaks, the group just stops being exclusive.

HTML

<details name="faq" open>
  <summary>Do the panels close each other?</summary>
  <div class="accordion-body">Yes.</div>
</details>
<details name="faq">
  <summary>How much CSS does that take?</summary>
  <div class="accordion-body">None.</div>
</details>

Animated open and close

A closed <details> does not render its content, so there is no height to transition from. Swapping it for a checkbox keeps the panel in the layout the whole time. The panel is a one row grid sized 0fr when closed and 1fr when open, and because grid-template-rows is animatable the row grows to the content's own height with no pixel value to guess. The padding sits on the element inside the grid child, not on the wrapper, or the closed panel would keep a visible strip.

The content never leaves the layout. Only the grid row it sits in changes size, and that is a property the browser can interpolate.

The native disclosure semantics. A screen reader announces a checkbox, not an expandable section, and you have to keep the input focusable yourself.

Yes. Checkboxes are independent. Swap them for radios sharing one name and the group turns exclusive instead.

HTML

<div class="acc-item">
  <input type="checkbox" id="acc-1" class="acc-check">
  <label class="acc-head" for="acc-1">Why does the height animate?</label>
  <div class="acc-panel">
    <div class="acc-panel-inner">
      <p>The content never leaves the layout.</p>
    </div>
  </div>
</div>

CSS

/* visually hidden, but still focusable: display:none
   would drop it out of the tab order entirely */
.acc-check {
  position: absolute;
  opacity: 0;
  width: 1px;
  height: 1px;
}

.acc-head {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: .9rem 1.1rem;
  background: #141415;
  cursor: pointer;
  font-weight: 600;
  user-select: none;
}

.acc-head::after {
  content: "+";
  font-size: 1.2rem;
  color: #b8ff57;
  transition: transform .25s;
}

/* one row, sized by a fraction that is animatable */
.acc-panel {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows .28s ease;
}

.acc-check:checked ~ .acc-panel {
  grid-template-rows: 1fr;
}

/* min-height:0 lets the row actually reach zero */
.acc-panel-inner {
  overflow: hidden;
  min-height: 0;
}

/* padding lives in here, not on .acc-panel, or the
   closed panel keeps a visible strip of background */
.acc-panel-inner p {
  margin: 0;
  padding: .9rem 1.1rem;
  border-top: 1px solid #2a2a2d;
  font-size: .9rem;
  color: #88888f;
}

.acc-check:checked ~ .acc-head::after {
  transform: rotate(45deg);
}

.acc-check:focus-visible ~ .acc-head {
  outline: 2px solid #b8ff57;
  outline-offset: -2px;
}

@media (prefers-reduced-motion: reduce) {
  .acc-panel,
  .acc-head::after { transition: none; }
}

How it works

The <details> element has built-in open/closed state managed by the browser. The open attribute is added and removed natively, with no JavaScript involved. CSS targets details[open] to style the expanded state and animates the + icon with a rotate transform.

<summary> has to be the first child of <details>. That is not a style preference, it is how the parser decides what the toggle is. Put a <div> above it and the browser treats the summary as ordinary flow content, renders its own empty toggle, and the panel stops collapsing. Everything after the summary is the panel, and it can be any markup, including another <details>.

Two rules remove the default disclosure triangle, because browsers disagree about what draws it. summary { list-style: none } handles Firefox and current Chromium, which paint the triangle as a list marker. Older Safari uses a proprietary pseudo-element instead, so summary::-webkit-details-marker { display: none } covers that. Once the marker is gone, summary::after { content: "+" } gives you an indicator you can position and animate. Rotating a plus sign 45 degrees turns it into a cross, which is why the open state is a single transform: rotate(45deg) rather than a second character swap.

The panel height cannot be transitioned on a <details> element directly. While it is closed the content is not rendered at all, so there is no height for the browser to animate from. There is no intermediate frame to interpolate. The checkbox variant below works around this by never removing the content: the panel is a grid whose single row is sized 0fr when closed and 1fr when open, with overflow: hidden on the child. grid-template-rows is an animatable property, so the row grows smoothly from nothing to the content's natural height, and no fixed pixel height has to be guessed in advance.

The name attribute makes a group of <details> elements behave like a radio group. Give several of them the same name and the browser closes the others when one opens. It is the same exclusivity that hidden radio inputs provide on tab navigation and pagination, except the state lives on the element instead of in a hidden form control, and no selector work is required. Older browsers ignore the attribute, which leaves every panel independent rather than broken.

For a single panel with one trigger, the checkbox pattern on show hide content covers the same ground with less markup. An accordion is worth the extra structure when several panels share a heading strip and the reader is choosing between them.

CSS properties used

list-style
none on summary removes the disclosure triangle in Firefox and current Chromium, which draw it as a list marker.
content
Generates the + indicator on summary::after. Any string, character, or generated counter works here.
transform
rotate(45deg) turns the plus into a cross for the open state, so one character serves both states.
transition
Animates the indicator rotation and the background change. On the checkbox variant it also animates grid-template-rows.
grid-template-rows
0fr to 1fr on a one-row grid is what makes the checkbox panel slide open without a hard-coded height.
overflow
hidden on the grid child clips the content while the row is shorter than it. Without it the text spills out of the collapsed panel.

Browser support

FeatureChromeFirefoxSafariEdge
details1249679
content423.112
transition455.112
grid575210.116
::marker8668No86

Figures come from Can I Use for Details and Summary, CSS generated content, CSS transitions, CSS Grid, and the ::marker pseudo-element. Safari does not style ::marker on a summary, which is why the marker is removed with list-style: none plus the older -webkit-details-marker rule rather than by styling ::marker directly. The name attribute that drives the exclusive variant has no Can I Use entry yet. It works in current Chrome, Edge, Safari, and Firefox, and browsers that do not recognize it simply leave every panel independent.

Accessibility notes

<details> and <summary> give you a real disclosure control with no ARIA at all. The summary is focusable, Enter and Space toggle it, and screen readers announce the expanded or collapsed state along with its label. Adding aria-expanded yourself is not just unnecessary, it can conflict with the state the browser is already reporting. Do not nest a <button> or a link inside the summary either: it is reachable by keyboard, but activating it also toggles the panel, so the reader gets an action they did not ask for.

The checkbox variant is where accessibility gets spent. A checkbox hidden with display: none is removed from the tab order entirely, which leaves the accordion operable by mouse only. Hide it with position: absolute; opacity: 0 instead, so it keeps focus, and put the focus ring on the label with :focus-visible. Even then a screen reader announces it as a checkbox rather than a disclosure, and the reader has to infer what checking it does. Use it when the animation matters more than the announcement, and use <details> when the content does.

Content inside a closed <details> stays in the accessibility tree as collapsed content that can be reached by opening the panel. Content inside a display: none panel is gone from that tree completely, and it is skipped by find in page as well. That difference is the strongest argument for the native element on anything a reader might search for, such as documentation or a support FAQ.

The stylesheet turns off the indicator rotation under prefers-reduced-motion: reduce. The panel still opens, it just arrives without the spin and without the height slide.

What you can build with it

  • FAQ pages. One question per summary, the answer in the panel. Pair it with FAQPage structured data so the questions are machine readable as well as collapsible.
  • Product specifications. Dimensions, materials, shipping and returns as separate panels, which keeps a long product page scannable without pushing the buy button below the fold.
  • Settings groups. Advanced options folded away behind a summary, so the common controls stay visible and the rare ones stay reachable.
  • Changelogs and release notes. The version number and date in the summary, the full list of changes in the panel.
  • Mobile navigation sections. Nested <details> inside an off canvas panel gives a multi level menu with no script and no focus management to write.

Mistakes worth avoiding

  • Putting anything before <summary> inside <details>. The browser only treats the first child as the toggle, so a wrapper div above it leaves you with a panel that never collapses and an empty triangle you cannot click.
  • Removing the marker with only summary::-webkit-details-marker. Firefox keeps drawing its triangle, because Firefox paints it as a list marker. Both that rule and list-style: none have to be present.
  • Transitioning height or max-height on the panel and expecting it to animate. Closed <details> content is not rendered, so there is no starting height. A max-height guess appears to work until the content is taller than the guess, at which point the panel clips.
  • Putting the padding on the grid wrapper in the animated variant instead of on the element inside it. Padding does not collapse with the row, so a closed panel keeps a visible strip of background and the accordion never looks properly shut.
  • Hiding the checkbox with display: none in the animated variant. That removes it from the tab order, and the accordion becomes unusable for anyone not holding a mouse.

Frequently asked questions

How do I make only one accordion panel open at a time?
Give every <details> in the group the same name attribute. The browser then treats them as a radio group and closes the open one when another is clicked. If you need to support browsers that ignore the attribute, the hidden radio input technique from the tab navigation example produces the same exclusivity in CSS.
Can I animate a `<details>` element opening and closing?
Not the element itself. Its content is not rendered while it is closed, so there is no height to transition from. The workaround is the checkbox variant on this page, where the panel stays in the layout as a one-row grid and the row transitions from 0fr to 1fr.
Why is the disclosure triangle still visible in Firefox?
Because the rule you used only removes the Chromium and older Safari marker. Firefox draws it as a list marker, so summary { list-style: none } is the rule that clears it there.
Is content inside an accordion indexed by search engines?
The text sits in the HTML that is served, not in something fetched afterwards, so a crawler reads it on the first request. Adding FAQPage structured data alongside the markup describes the question and answer pairs explicitly rather than leaving them to be inferred from the layout.
Does a CSS accordion need JavaScript?
No. <details> handles its own state, and the animated variant uses a checkbox and a label. The only reason to add script is if you want to persist which panel was open between visits.