Details/Summary Element
The <details> and <summary> elements create a native disclosure widget — click (or keyboard-activate) a heading to reveal or hide related content. No JavaScript is required for the core expand/collapse behavior, which makes details ideal for FAQs, progressive disclosure, and simple navigation menus.
Unlike custom accordion components built from buttons and aria-expanded, the browser wires up keyboard support, accessibility mappings, and the open/closed state for you. Modern HTML also adds a name attribute so a group of details behaves like an exclusive accordion — opening one closes others with the same name.
This guide covers anatomy, the open attribute, the toggle event, exclusive name grouping, nesting, marker styling, [open] selectors, FAQ and nav patterns, accessibility, when not to use details, comparisons with dialog/popover, JS enhancement, CSS-only tricks, browser quirks, and best practices.
info
A disclosure widget has two parts. <details> wraps everything. Its first meaningful interactive child should be <summary> — the always-visible label users activate. Everything after the summary is the disclosure content, hidden until open.
| 1 | <details> |
| 2 | <summary>Shipping policy</summary> |
| 3 | <p>Orders ship within 2 business days.</p> |
| 4 | <ul> |
| 5 | <li>Standard: 5–7 days</li> |
| 6 | <li>Express: 2–3 days</li> |
| 7 | </ul> |
| 8 | </details> |
If you omit <summary>, browsers invent a default label (often “Details”). Always provide an explicit, descriptive summary.
Add the boolean open attribute to render the widget expanded on first paint. Removing it (or setting details.open = false in JS) collapses the content. The attribute reflects live state as the user toggles.
| 1 | <!-- Expanded by default --> |
| 2 | <details open> |
| 3 | <summary>Getting started</summary> |
| 4 | <p>Install the CLI, then run <code>init</code>.</p> |
| 5 | </details> |
| 6 | |
| 7 | <!-- Collapsed by default --> |
| 8 | <details> |
| 9 | <summary>Advanced configuration</summary> |
| 10 | <p>Override defaults in <code>config.json</code>.</p> |
| 11 | </details> |
best practice
Whenever a details element opens or closes, it fires a toggle event (after the state changes). Listen to sync analytics, lazy-load content, or update adjacent UI. Check event.newState in supporting browsers, or read details.open.
| 1 | const panel = document.querySelector("#faq-1"); |
| 2 | |
| 3 | panel.addEventListener("toggle", (event) => { |
| 4 | // Modern: event.newState === "open" | "closed" |
| 5 | const isOpen = panel.open; |
| 6 | if (isOpen) { |
| 7 | console.log("Opened", panel.id); |
| 8 | // e.g. lazy-load a chart inside the panel |
| 9 | } |
| 10 | }); |
note
Modern browsers support name on <details>. Details that share the same name form an exclusive accordion group: opening one closes the others. No JavaScript required.
| 1 | <details name="pricing" open> |
| 2 | <summary>Hobby</summary> |
| 3 | <p>Free for personal projects.</p> |
| 4 | </details> |
| 5 | |
| 6 | <details name="pricing"> |
| 7 | <summary>Pro</summary> |
| 8 | <p>Team features and priority support.</p> |
| 9 | </details> |
| 10 | |
| 11 | <details name="pricing"> |
| 12 | <summary>Enterprise</summary> |
| 13 | <p>SSO, audit logs, and SLAs.</p> |
| 14 | </details> |
warning
You can nest details inside details for hierarchical disclosure (docs outlines, file trees, nested FAQs). Keep nesting shallow — two levels is usually enough. Deep nesting becomes hard to navigate with a keyboard and visually noisy.
| 1 | <details open> |
| 2 | <summary>API reference</summary> |
| 3 | <details> |
| 4 | <summary>Authentication</summary> |
| 5 | <p>Send a Bearer token in the Authorization header.</p> |
| 6 | </details> |
| 7 | <details> |
| 8 | <summary>Rate limits</summary> |
| 9 | <p>1000 requests per hour on the free plan.</p> |
| 10 | </details> |
| 11 | </details> |
Browsers show a disclosure triangle next to the summary. Style it with summary::marker (standard) and hide the legacy WebKit pseudo-element when customizing. Many designs set list-style: none on summary and draw a custom indicator with ::before.
| 1 | summary { |
| 2 | cursor: pointer; |
| 3 | list-style: none; /* hide default marker in supporting browsers */ |
| 4 | } |
| 5 | |
| 6 | summary::-webkit-details-marker { |
| 7 | display: none; /* Safari / older Chromium */ |
| 8 | } |
| 9 | |
| 10 | summary::marker { |
| 11 | content: ""; /* belt-and-suspenders */ |
| 12 | } |
| 13 | |
| 14 | summary::before { |
| 15 | content: "▸"; |
| 16 | display: inline-block; |
| 17 | margin-right: 0.5rem; |
| 18 | color: #00ff41; |
| 19 | transition: transform 150ms ease; |
| 20 | } |
| 21 | |
| 22 | details[open] > summary::before { |
| 23 | transform: rotate(90deg); |
| 24 | } |
pro tip
The [open] attribute selector is the primary hook for open-state styling. Style the summary differently when expanded, animate content height carefully (details height animation is historically tricky), and emphasize the active panel in a group.
| 1 | details { |
| 2 | border: 1px solid #222; |
| 3 | border-radius: 8px; |
| 4 | background: #0d0d0d; |
| 5 | } |
| 6 | |
| 7 | details[open] { |
| 8 | border-color: #00ff41; |
| 9 | } |
| 10 | |
| 11 | details[open] > summary { |
| 12 | color: #00ff41; |
| 13 | border-bottom: 1px solid #222; |
| 14 | } |
| 15 | |
| 16 | details[open] > *:not(summary) { |
| 17 | animation: fade-in 160ms ease; |
| 18 | } |
| 19 | |
| 20 | @keyframes fade-in { |
| 21 | from { opacity: 0; transform: translateY(-4px); } |
| 22 | to { opacity: 1; transform: none; } |
| 23 | } |
FAQs are the most common details use case. Use a heading level around the group, keep each summary a concise question, and put the answer in flowing content after the summary. For SEO, ensure answers are in the HTML (not loaded only after click via empty shells) so crawlers can index them — details content is in the DOM even when collapsed.
| 1 | <section aria-labelledby="faq-heading"> |
| 2 | <h2 id="faq-heading">Frequently asked questions</h2> |
| 3 | |
| 4 | <details name="faq"> |
| 5 | <summary>Do I need JavaScript for details?</summary> |
| 6 | <p>No. Expand/collapse works with HTML alone.</p> |
| 7 | </details> |
| 8 | |
| 9 | <details name="faq"> |
| 10 | <summary>Are answers indexed by search engines?</summary> |
| 11 | <p>Yes — collapsed details content remains in the document.</p> |
| 12 | </details> |
| 13 | |
| 14 | <details name="faq"> |
| 15 | <summary>Can multiple panels be open?</summary> |
| 16 | <p>Yes, unless they share a <code>name</code> for exclusivity.</p> |
| 17 | </details> |
| 18 | </section> |
Summary is keyboard-activatable (Enter / Space in supporting browsers). Screen readers expose the disclosure as an expandable widget. Keep summary text clear; do not nest interactive elements (links, buttons) inside the summary — that creates competing activation targets and confusing announcements.
| Do | Don't |
|---|---|
| Use a short, descriptive summary | Put buttons/links inside summary |
| Ensure visible focus styles | Remove outline without a replacement |
| Keep related content after summary | Move content with CSS order hacks that break SR reading |
| Test with keyboard only | Assume click-only users |
| Use headings for the FAQ section | Fake headings with styled divs in summary only |
best practice
Details is a disclosure widget, not a universal tabs/accordion replacement for every IA pattern. Prefer dedicated tab patterns (role="tablist") when panels are peer views of equal importance and users switch frequently without a “summary vs body” mental model. Prefer dialogs for modal tasks, and popovers for ephemeral menus.
Complex tab UIs
Settings pages with many peer panels, dashboards with persistent panel chrome, and keyboard roving tabindex expectations belong to a proper tabs pattern — not nested details.
Primary app navigation on desktop
Always-visible nav is often better for wayfinding. Use details for overflow sections or mobile collapse, not as the only IA.
Content that must animate height precisely
Smooth height transitions on details are historically inconsistent. If motion design is a hard requirement, measure carefully or use a JS-enhanced accordion with explicit height animation.
| Need | Use | Why |
|---|---|---|
| Inline expand/collapse | <details> | In-flow disclosure, no overlay |
| Modal confirm / form | <dialog> | Focus trap, backdrop, returnValue |
| Dropdown menu / tooltip | popover | Top layer + light dismiss |
| Exclusive FAQ accordion | details name | Native exclusive group |
| Peer tab panels | Tabs pattern | Equal-weight views, arrow-key UX |
Start with HTML-only details, then enhance. Common enhancements: exclusive accordion polyfill, deep-linking (#faq-shipping opens a panel), analytics on toggle, and lazy-rendering heavy content the first time a panel opens.
| 1 | // Exclusive accordion polyfill when name is unsupported |
| 2 | function enhanceExclusiveAccordions(groupName) { |
| 3 | const items = [...document.querySelectorAll(`details[name="${groupName}"]`)]; |
| 4 | items.forEach((item) => { |
| 5 | item.addEventListener("toggle", () => { |
| 6 | if (!item.open) return; |
| 7 | items.forEach((other) => { |
| 8 | if (other !== item) other.open = false; |
| 9 | }); |
| 10 | }); |
| 11 | }); |
| 12 | } |
| 13 | |
| 14 | // Deep-link: /faq#shipping opens that panel |
| 15 | const id = location.hash.slice(1); |
| 16 | if (id) { |
| 17 | const target = document.getElementById(id); |
| 18 | if (target?.tagName === "DETAILS") target.open = true; |
| 19 | } |
Beyond markers, you can swap summary labels with ::after content, grid-align chevrons, and use details[open] summary ~ * to style disclosed siblings. Avoid relying on display: none overrides that fight the UA — let the element manage visibility.
| 1 | /* Push chevron to the end */ |
| 2 | summary { |
| 3 | display: flex; |
| 4 | align-items: center; |
| 5 | justify-content: space-between; |
| 6 | gap: 1rem; |
| 7 | } |
| 8 | |
| 9 | summary::after { |
| 10 | content: "Expand"; |
| 11 | font-size: 0.75rem; |
| 12 | color: #525252; |
| 13 | } |
| 14 | |
| 15 | details[open] > summary::after { |
| 16 | content: "Collapse"; |
| 17 | color: #00ff41; |
| 18 | } |
| Quirk | Impact | Mitigation |
|---|---|---|
| Marker pseudo differences | Inconsistent triangles | Hide UA marker; use ::before |
| Height animation | Hard to animate open | Fade/slide content, or JS measure |
| name exclusivity support | Multi-open on old browsers | Small toggle polyfill |
| Summary + interactive children | Broken activation | Never nest links/buttons in summary |
| print styles | Collapsed content may hide | Force open / display block in @media print |
| 1 | @media print { |
| 2 | details { |
| 3 | open: true; /* not universally supported as property */ |
| 4 | } |
| 5 | details, |
| 6 | details * { |
| 7 | display: block !important; |
| 8 | } |
| 9 | summary { |
| 10 | font-weight: 700; |
| 11 | } |
| 12 | } |
Write summary as a question or label
Users decide whether to expand based on the summary alone. Vague labels like “More” waste the pattern.
Keep default closed unless context demands open
Open the active nav section or the first FAQ when it helps orientation — not everything.
Prefer name for exclusive FAQs
Exclusive groups reduce scroll length and match user expectations for many FAQ UIs.
Enhance, don't reinvent
Start with native details. Add JS only for deep links, polyfills, or lazy loading.
Test print and find-in-page
Ensure critical content remains reachable when collapsed visuals differ across engines.
danger
note
note
info
Community
Get help on Slack, Discord or VIP
Stuck on a guide? Join the community and ask.