Accordion and FAQ
Built on details and summary — open, close and keyboard support with no JavaScript.
<div class="faq">
<details name="faq" open>
<summary>Does linen shrink?</summary>
<div class="answer">
<p>A little on the first wash, which is why good linen is pre-washed before it is cut.
Ours loses about two percent and then stays put.</p>
</div>
</details>
<details name="faq">
<summary>Can I tumble dry it?</summary>
<div class="answer">
<p>You can, on low. Line drying keeps the fibres softer for longer, and linen dries faster
than almost anything else, so it is rarely worth the machine.</p>
</div>
</details>
<details name="faq">
<summary>Why does it crease so much?</summary>
<div class="answer">
<p>Because the fibre is stiff and hollow, which is the same reason it is cool to wear.
Creasing is a property of the cloth, not a defect in it.</p>
</div>
</details>
<details name="faq">
<summary>How should I store it?</summary>
<div class="answer">
<p>Folded and loose, somewhere dry. Long spells on a wire hanger stretch the shoulders,
and plastic covers trap moisture.</p>
</div>
</details>
</div>* { box-sizing: border-box; margin: 0; }
body { font: 16px/1.6 system-ui, -apple-system, sans-serif; padding: 24px; background: #fff; }
.faq {
max-width: 620px;
margin: 0 auto;
border: 1px solid #e2e7ef;
border-radius: 12px;
overflow: hidden;
}
details + details { border-top: 1px solid #e2e7ef; }
summary {
display: flex; /* also removes the default triangle */
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 16px 20px;
font-size: .98rem;
font-weight: 500;
color: #16202e;
cursor: pointer;
list-style: none; /* Firefox */
user-select: none;
}
summary::-webkit-details-marker { display: none; } /* older Safari */
summary:hover { background: #f6f8fb; }
summary:focus-visible { outline: 2px solid #3b4fe4; outline-offset: -2px; }
/* the chevron */
summary::after {
content: "";
width: 9px;
height: 9px;
flex-shrink: 0;
border-right: 2px solid #8593ab;
border-bottom: 2px solid #8593ab;
transform: rotate(45deg) translate(-2px, -2px);
transition: transform .18s ease;
}
details[open] > summary { background: #f6f8fb; }
details[open] > summary::after { transform: rotate(-135deg) translate(-2px, -2px); }
.answer { padding: 0 20px 18px; color: #5b6b83; font-size: .94rem; }
.answer p + p { margin-top: 12px; }
/* smooth open and close, where the browser supports interpolating to auto */
details::details-content {
block-size: 0;
overflow: hidden;
transition: block-size .24s ease, content-visibility .24s allow-discrete;
}
details[open]::details-content { block-size: auto; }
@media (prefers-reduced-motion: reduce) {
summary::after { transition: none; }
details::details-content { transition: none; }
}How it works
<details> and <summary> are a disclosure widget built into HTML. They open, close, respond to Enter and Space, and announce their expanded state — with no script at all. Most accordion libraries exist to reimplement this badly.
display: flex on summary removes the marker. The default triangle disappears as soon as you change the display type, which is the cleanest way to get rid of it. list-style: none covers Firefox and the ::-webkit-details-marker rule covers older Safari.
The name attribute makes it exclusive. Giving several <details> elements the same name turns them into a radio group: opening one closes the others. This is a recent addition and it removes the last reason people reached for JavaScript here. Drop the attribute and they open independently.
The chevron is two borders on a rotated pseudo-element. No icon file, no font, and it inherits nothing you have to keep in sync — the rotation flips with details[open].
::details-content is what finally allows animation. Height could never be transitioned to auto, which is why every accordion library measured the content and animated a pixel value. The new pseudo-element plus allow-discrete handles it in CSS. Browsers without support open instantly, which is the behaviour everyone had until recently.
An inset focus ring. outline-offset: -2px draws the ring inside the summary, so it is not clipped by the container's overflow: hidden.
Accessibility notes
<summary> is exposed as a button with an expanded state, so a screen reader announces "Does linen shrink? button, collapsed" and updates when it opens. That is the whole contract, and you get it for nothing.
Do not add role="button" or aria-expanded by hand. Both are already implied, and adding them can conflict with the native semantics.
Keep the summary text short and meaningful on its own. Screen reader users often pull up a list of all the buttons on a page, and "Read more" four times in a row tells them nothing.
Making it yours
For an FAQ page, add FAQPage structured data that matches the visible questions. The markup must describe what is actually on the page — marking up an FAQ the visitor cannot see is a structured data violation.
To have the first item open on load, put open on it as shown. With name set, only one item may carry open in the initial HTML.
Content inside a closed <details> is still in the DOM, so browser find-in-page can reach it and modern browsers will open the section automatically when it matches.
Related templates
Button set
Primary, secondary, ghost and danger, in three sizes, with every state covered.
HTMLCSSTailwindLoaders and skeletons
Six loading indicators in pure CSS, plus the skeleton pattern that usually beats all of them.
HTMLCSSModal dialog
A modal built on the native dialog element — focus trap and backdrop included, for free.
HTMLCSSJavaScriptTooltips
CSS-only tooltips on four sides, plus the anchor-positioning version.
HTMLCSSCheck your version
Once you have edited this, the HTML validator will catch any tag you left unclosed, and the formatter will tidy the indentation. Both run in your browser.