Blog / Coding tips

HTML Details and Summary: Accordion Without JavaScript (Tested)

Cover image for HTML Details and Summary: Accordion Without JavaScript (Tested)

The HTML details and summary elements give you a working accordion with no JavaScript at all. Put the always-visible heading inside summary, put the hidden content after it inside details, and the browser handles clicking, keyboard support (Enter and Space) and the open or closed state for you. Add the open attribute to show a panel by default, and give several details elements the same name attribute to make them behave like a classic accordion where opening one closes the others. You can remove the default triangle with list-style: none on the summary (plus ::-webkit-details-marker for older Safari), replace it with your own icon, and even animate the opening with a few lines of modern CSS.

I built and tested the accordion in this guide in headless Chrome 154 on 7 October 2026, using a script that clicks the panels, presses keys and reads what a screen reader would announce. That test found one real trap, a "+" sign that gets read out as part of every heading, and I show the one-line fix. Everything else here is plain HTML and CSS you can paste into any page, including a WordPress or admin editor that strips scripts.

How the details and summary elements work

Here's the smallest possible example:

<details>
  <summary>What is your returns policy?</summary>
  <p>You can return unused items within 30 days for a full refund.</p>
</details>

The browser shows only the summary line, with a small triangle in front of it. Clicking the line (or tabbing to it and pressing Enter or Space) reveals everything else inside details, and the triangle points down. Clicking again hides it. There's no script, no ARIA attributes to manage and no class to toggle.

A few rules make it behave properly:

  • summary must be the first child of details. If it's missing, the browser invents a heading that says "Details", which is rarely what you want.
  • Only one summary per details. Everything after it is the hidden content, and it can be any HTML: paragraphs, lists, images, forms or even another details.
  • The open attribute controls the state. Write <details open> to show a panel when the page loads. The browser adds and removes the attribute as people click, so you can style the open state with details[open].
  • Hidden content is still in the page. Search engines can read it, and anyone can see it with View Source, so it isn't a way to hide secrets. Chrome's find-in-page (Ctrl+F) is documented to open a closed panel when it finds a match inside; I couldn't test that in headless Chrome, where the scripted window.find() found the word but left the panel closed.

This makes details ideal for FAQs, "show more" sections, long terms and conditions, and filters on a product list. It's one of the semantic elements I recommend in my guide to HTML semantic tags, because the element itself tells browsers and assistive technology exactly what it is.

Build an accordion that opens one at a time

A set of details elements is already an accordion where every panel opens independently. For the classic "only one open at a time" behaviour, you used to need JavaScript. Now you just give them the same name, the same way radio buttons in a group share one.

<section class="faq">
  <h2>Delivery questions</h2>
  <details name="delivery" open>
    <summary>How long does UK delivery take?</summary>
    <div class="answer"><p>Standard delivery takes 2 to 4 working days.</p></div>
  </details>
  <details name="delivery">
    <summary>Do you deliver to Northern Ireland?</summary>
    <div class="answer"><p>Yes, at the same price as Great Britain.</p></div>
  </details>
  <details name="delivery">
    <summary>Can I change my address after ordering?</summary>
    <div class="answer"><p>Yes, until the order is dispatched.</p></div>
  </details>
</section>

My test script loaded the page, clicked panels and pressed keys, and recorded the state of the three panels after each step:

Chrome: Chrome/154.0.8037.57
on load:            open, closed, closed
click 2nd summary:  closed, open, closed
Enter on 3rd:       closed, closed, open
Space on 3rd:       closed, closed, closed
toggle events: #0 closed->open | #1 closed->open | #0 open->closed | #2 closed->open | #1 open->closed | #2 open->closed
  • On load, only the first panel was open, because it has the open attribute.
  • Clicking the second summary opened it and closed the first automatically. That's the name="delivery" grouping at work.
  • Pressing Enter on the focused third summary opened it and closed the second. Keyboard users get exactly the same behaviour as mouse users.
  • Pressing Space on the same summary closed it again, so you can still have all panels closed.
  • The toggle events show every change, with the old and new state. The first one fired on load, because the browser treats the initial open attribute as a change.

The name attribute is supported in current Chrome, Edge, Firefox and Safari. In an older browser it's simply ignored, and the panels open independently, which is a perfectly acceptable fallback. Use a different name for each separate accordion on the page, or the panels in both groups will close each other.

Listening for changes with the toggle event

You don't need JavaScript for the accordion itself, but you might want it for analytics or to save which panel was open. Every details element fires a toggle event after it opens or closes, and the event tells you both states:

<script>
  window.toggleLog = [];
  document.querySelectorAll("details").forEach((d, i) =>
    d.addEventListener("toggle", e => window.toggleLog.push(`#${i} ${e.oldState}->${e.newState}`)));
</script>

You can also open a panel from code with element.open = true. My test did that on a separate, unstyled details element and confirmed that open was true straight afterwards. That's handy for opening the panel that matches the URL fragment, so a link like /faq#delivery can land on the right answer already expanded.

Style the summary: remove the arrow or change the icon

The default triangle is a marker, the same kind of thing as a bullet on a list item. My test read the computed styles of a plain summary and a styled one:

styled summary: {"display":"flex","listStyle":"none","after":"\"+\""}
plain summary: {"display":"list-item","marker":"disclosure-closed"}

The plain summary is display: list-item with a disclosure-closed marker. To remove it, set list-style: none, which works in Chrome, Edge and Firefox. Older versions of Safari draw the marker with a separate pseudo-element, so add summary::-webkit-details-marker { display: none; } too. Switching the summary to display: flex (or block) also removes the marker, because it's no longer a list item, and flexbox makes it easy to push your own icon to the right-hand edge. If you're not sure which layout to reach for, my comparison of CSS flexbox and grid covers this kind of one-row alignment.

Here's the full stylesheet from the tested accordion:

  .faq details { border: 1px solid #c8d0e0; border-radius: 8px; margin-block: 8px; }
  .faq summary {
    cursor: pointer; padding: 12px 16px; font-weight: 600;
    list-style: none;                 /* removes the marker in Chrome/Firefox */
    display: flex; justify-content: space-between; align-items: center;
  }
  .faq summary::-webkit-details-marker { display: none; }   /* Safari */
  .faq summary::after { content: "+" / ""; font-size: 1.25rem; }
  .faq details[open] > summary::after { content: "−" / ""; }
  .faq summary:focus-visible { outline: 3px solid #1d70b8; outline-offset: 2px; }
  .faq .answer { padding: 0 16px 12px; }

The icon is a "+" when the panel is closed and a "−" (a real minus sign) when it's open, using details[open] > summary::after. Use the child combinator >, not a space. With a space, a nested details inside an open panel would show the "−" even while it's closed, because its parent is open.

Keep cursor: pointer so the summary looks clickable, and keep a visible focus style. The :focus-visible outline in this example only appears for keyboard users, so mouse users don't see a box after clicking, but people tabbing through the page always know where they are. Don't remove the marker without adding another visual cue, or sighted users won't know the line expands.

Style the open panel and its content

Once the marker is sorted, most of the design work is on the open state. A few patterns I use on real sites:

  • Highlight the open panel. details[open] { border-color: #1d70b8; } makes it obvious which answer is showing, which helps on long FAQ pages where the open panel can scroll out of view.
  • Give the content its own padding. Wrap the answer in a div (the .answer class above) rather than padding details itself, so the summary keeps its full clickable area and the content lines up with the question text.
  • Make the whole row clickable. Because summary is a block-level flex container here, the click target stretches across the full width, not just the text. That's much easier to hit on a phone.
  • Use ::details-content for content styles. In browsers that support it, details::details-content { padding: 0 16px; } styles the hidden part without an extra wrapper. I still keep the wrapper for older browsers.
  • Keep long summaries readable. If the question might wrap onto two lines, align-items: center keeps the icon vertically centred, and gap: 1rem on the flex container stops the text running into it. For one-line summaries in a narrow sidebar, the ellipsis technique from my post on CSS text-overflow ellipsis also works inside a summary, as long as you add min-width: 0 to the text element.

The screen reader trap: generated icons get read out

This is the bug my test caught. Text added with ::after { content: "+" } becomes part of the summary's accessible name, the text a screen reader announces. Here's what Chrome reported, for my first version and the fixed one:

accordion.html        accessible name: "How long does UK delivery take? −"  ::after content: "−"
accordion-fixed.html  accessible name: "How long does UK delivery take?"  ::after content: "−" / ""

With the plain content: "+", a screen reader user hears "How long does UK delivery take? minus" or "plus" on every heading. The fix is CSS's alternative-text syntax: content: "+" / "". The part after the slash is the text for assistive technology, and an empty string means "say nothing". The icon still shows on screen, and the accessible name is now just the question. Browsers that don't understand the slash syntax ignore the whole declaration, so if you need to support old browsers, write the plain content: "+" line first and the "+" / "" line after it.

Chrome reported the summary's role as a disclosure triangle, which screen readers announce as a button that can be expanded or collapsed, and they read the expanded or collapsed state for you. That's the main reason I prefer details to a home-made accordion of divs and click handlers: you get the right semantics without writing a single ARIA attribute.

Animate opening and closing with CSS

For years, details couldn't be animated, because the content jumped straight from hidden to visible. Two newer CSS features fix that: the ::details-content pseudo-element, which selects everything after the summary, and interpolate-size: allow-keywords, which lets the browser animate to and from auto heights.

  /* Animation (progressive enhancement) */
  .faq { interpolate-size: allow-keywords; }
  .faq details::details-content {
    block-size: 0; overflow: hidden;
    transition: block-size .3s ease, content-visibility .3s allow-discrete;
  }
  .faq details[open]::details-content { block-size: auto; }
  @media (prefers-reduced-motion: reduce) {
    .faq details::details-content { transition: none; }
  }

To check it really animates, my script measured the height of the second panel just before clicking, 100 milliseconds after, and 600 milliseconds after, once with normal settings and once with the reduced-motion preference switched on:

reduced-motion=no-preference closed=49px  after 100ms=78px  after 600ms=111px
reduced-motion=reduce        closed=49px  after 100ms=111px  after 600ms=111px

With normal settings, the panel was part-way open at 100 ms (78 px, between the closed 49 px and the open 111 px), so the transition ran. With reduced motion, it was fully open at 100 ms, because the media query turns the transition off. Always include that query. Sliding and zooming effects can make some people feel unwell, and the setting is easy to respect.

The content-visibility .3s allow-discrete part matters too. The browser hides closed content with content-visibility, and without allow-discrete the content would vanish instantly when closing, before the height had shrunk. Browsers that don't support ::details-content or interpolate-size just open and close instantly, which is exactly how details has always worked, so this is a safe progressive enhancement. Don't animate with max-height hacks or JavaScript height measuring any more; this is shorter, and it doesn't break when the content changes size.

When not to use details and summary

The element is a disclosure widget: one heading that shows or hides one block of content. That's a narrower job than it first looks, and plenty of interface patterns that look like an accordion behave differently when you use them with a keyboard or a screen reader. Before reaching for details, ask whether the hidden content is optional extra detail. If it is, details is perfect. If it isn't, one of these is probably a better fit:

  1. Tabs. A row of tabs where exactly one panel is always visible needs a tab pattern with role="tablist" and arrow-key navigation, not an accordion.
  2. Navigation menus. A details dropdown can work for a simple mobile menu, but it won't close when you click elsewhere on the page without some JavaScript.
  3. Important content. Anything a visitor must read, such as a price or a cancellation deadline, shouldn't be hidden behind a click.
  4. Headings in the summary. You can put an h3 inside summary, and it helps people who navigate by headings, but some screen readers then announce the heading role and lose the button role. Test with a real screen reader before relying on it. A plain text summary is the safe default.
  5. Form steps. For a multi-step form, use real pages or a step pattern with validation. My guide to HTML form input types and validation shows how much the browser can check for you before you reach for scripts.

Also remember that the open or closed state isn't saved. If someone opens a panel and refreshes the page, it's closed again. If that matters, save the state in localStorage from the toggle event, the same way my dark mode toggle with localStorage remembers a theme.

Common mistakes with details and summary

  1. Leaving out summary. The browser shows the word "Details" as the heading.
  2. Removing the marker with no replacement. Users can't tell the line expands. Add an icon and keep cursor: pointer.
  3. Generated icons read aloud. content: "+" is added to the accessible name. Use content: "+" / "".
  4. Using a descendant selector for the open state. details[open] summary also matches nested closed panels. Use details[open] > summary.
  5. Reusing one name for two accordions. Panels in both groups will close each other. Give each group its own name.
  6. Putting buttons or links inside summary. Clicking them also toggles the panel, which confuses everyone. Keep interactive elements in the content.
  7. Animating without prefers-reduced-motion. Always switch the transition off for people who ask for less motion.
  8. Removing the focus outline. Keyboard users lose their place. Style :focus-visible instead of hiding focus.

FAQ

How do I make an accordion with HTML only?

Use several details elements, each with a summary heading, and give them the same name attribute, for example <details name="faq">. The browser then lets only one panel in the group be open at a time. Clicking, keyboard support and the open state all work without JavaScript, and older browsers fall back to independent panels.

How do I remove the arrow from the summary element?

Set summary { list-style: none; } for Chrome, Edge and Firefox, and add summary::-webkit-details-marker { display: none; } for Safari. Setting display: flex or display: block on the summary also removes it. Add your own icon with summary::after and change it with details[open] > summary::after.

How do I make a details element open by default?

Add the open attribute: <details open>. The panel shows its content when the page loads, and visitors can still close it. In JavaScript, element.open = true or false opens and closes it, and the toggle event fires afterwards.

Can you animate the details element with CSS?

Yes, in current browsers. Transition the block-size of details::details-content from 0 to auto, set interpolate-size: allow-keywords on a parent, and include content-visibility with allow-discrete in the transition. My test measured the panel part-way open after 100 ms. Turn the transition off inside @media (prefers-reduced-motion: reduce).

Is the details element accessible?

Yes. Browsers expose the summary as an expandable button and announce whether it's expanded, and it works with the keyboard out of the box. Keep the summary text short, keep a visible focus style, and hide decorative icons from screen readers with the content: "+" / "" syntax. The MDN reference for the details element lists every attribute, including name.

// note

How to read this note.

This is a learning note from studying the web. It is one small topic, written so I can remember it. It is not a course and not a claim that I have finished the subject.

If a sentence is wrong, say so from the contact page and name this title. Drafts never appear here. Related notes, when they exist, are other published posts, and the same sample rule applies to each of them.

Related notes

PHP Sort Multidimensional Array by Value (usort, Tested)
Coding tips

PHP Sort Multidimensional Array by Value (usort, Tested)

usort($rows, fn($a, $b) => $a['grade'] <=> $b['grade']) sorts by one column. Descending, several columns, UK dates, case-insensitive names, keeping keys and array_multisort.

October 7, 2026 · 17 min read · 0 views