DEV Community

Cover image for Stop Hand-Rolling an Exclusive Accordion. `<details name>` Does It
Parsa Jiravand
Parsa Jiravand

Posted on Originally published at bestpractic.org

Stop Hand-Rolling an Exclusive Accordion. `<details name>` Does It

You're building an FAQ page. Ten questions, each collapsed by default, and — this part's in every design spec — only one answer open at a time. Click a second question, the first one closes. Standard accordion behavior, the kind you've built before you finish reading the ticket.

So you reach for <details> and <summary>, because it's the native disclosure widget and you'd rather not hand-roll aria-expanded toggling again. Ten of them render fine. Then you click the second one open while the first is still open, and both sit there, expanded, taking up the whole viewport. <details> never promised mutual exclusivity — every element manages its own open state, full stop.

So you write the loop everyone writes.

The fix everyone reaches for first

const items = document.querySelectorAll("details.faq-item");

items.forEach((item) => {
  item.addEventListener("toggle", () => {
    if (!item.open) return;
    items.forEach((other) => {
      if (other !== item) other.open = false;
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

It works. It's also more surface area than it looks: a querySelectorAll scoped to a class you have to remember to apply consistently, an event listener per item, a forEach over every item each time any panel opens, whether or not anything needs to close, and a bug waiting for the first time someone copy-pastes an FAQ block without re-running the script — say, into content injected after the page loads. Miss the re-init and you're back to every panel opening independently, silently, with no error to tell you why.

And that's the simple version. The moment product wants nested accordions, or two independent FAQ groups on the same page that shouldn't interact, this script needs scoping logic, and now it's a small state machine you're maintaining for something that reads, in the design file, as "an accordion."

What the browser already does with one word

Since late 2023 (Chrome 120, Safari 17.2; Firefox followed in 130, September 2024), <details> takes a name attribute. Give a group of them the same name, and the browser enforces the mutual exclusivity itself — open one, the others in that group close, automatically, with zero JavaScript:

<details name="faq">
  <summary>What's your refund policy?</summary>
  <p>Full refund within 30 days, no questions asked.</p>
</details>

<details name="faq">
  <summary>Do you offer team plans?</summary>
  <p>Yes — see the pricing page for per-seat rates.</p>
</details>

<details name="faq">
  <summary>Can I cancel anytime?</summary>
  <p>Yes, from account settings, effective end of billing period.</p>
</details>
Enter fullscreen mode Exit fullscreen mode

That's the whole implementation. It behaves like a radio group built out of disclosure widgets: same name value ties them together, opening one closes every other member of the group, and it works the instant you add another <details name="faq"> anywhere on the page — no re-running a script, no re-scoping a selector.

It's Baseline-available across current Chrome, Edge, Firefox, and Safari, which means you can rely on it in a production FAQ page today. Where support is missing, the fallback isn't broken — it's just every <details> reverting to independent behavior, exactly like today, so nothing crashes on an older browser. That's the quiet advantage of building on a native element instead of a div with ARIA bolted on: the un-enhanced state is still a working, keyboard-accessible disclosure widget, not a blank div waiting on JS that never ran.

The part that actually surprised me

I expected the grouping to require the elements to be siblings, or at least share a parent container — that's how the radio-input-plus-CSS trick works, and it's how most accordion libraries assume you'll structure the DOM. It doesn't. The name attribute groups by value alone (within one DOM tree — a shadow root starts its own groups). Two <details name="settings"> elements in completely different sections of the page, with unrelated markup between them, are still one exclusive group.

The spec allows it but advises against leaning on it: authors "should keep those related elements together in a containing element," because panels that close each other from disparate corners of a page have a relationship that's hard to discover — especially with a screen reader. It's mostly the thing to watch for: reuse a name by accident — paste a component, forget to change the group id — and two accordions you meant to be independent start closing each other's panels, and there's no console warning telling you why.

Where the JavaScript still earns its keep

Native grouping replaces the sibling-closing loop, not everything you might want an accordion to do. You'd still reach for a script if you need to:

  • Open a panel from outside a click — a "Contact support" button that opens the relevant answer still means setting .open = true yourself (the group then closes the others). Plain anchor links are the exception: in current browsers, following a #id that points at content inside a closed <details> expands it natively, and the name group closes whichever panel was open.
  • Persist state — remembering which panel was open across a reload is your localStorage write, same as any other piece of UI state.
  • React to which one opened — analytics on "which FAQ gets clicked most" still listens for the toggle event; check event.newState === "open", because the sibling the browser auto-closes fires its own toggle too (with newState: "closed").

None of that is the accordion's core behavior, though — it's things you'd bolt onto any accordion, native or not. The mutual-exclusivity plumbing, the part that used to be your bug to maintain, is gone.

Try both versions side by side below: the naive JS-loop accordion and the name-grouped native one, with a toggle to break the name value and watch two "independent" groups start fighting over the same state.

🎮 Try it yourself

▶️ Open the interactive playground →

Runs right in your browser — poke at it and watch the concept react live.

The lesson that generalizes past accordions

<details> picked up name grouping the same way <dialog> picked up native modal semantics and <input type="date"> picked up a whole calendar widget: the platform absorbing a pattern that used to be userland plumbing. The tell that it's worth checking MDN before you write the loop is exactly this shape of problem — "state that needs to stay consistent across a group of elements" — because that's precisely the kind of thing HTML has been quietly annexing from JavaScript for a few years now.

Next time you're about to write a forEach that closes siblings, checks a data- attribute, or manages "only one of these is open at a time," it's worth thirty seconds on MDN before the twentieth line of the script. Sometimes the browser already shipped it.

What's the last bit of state-management JavaScript you wrote that turned out to already be a native HTML attribute? I've got a second one of these for popover's popovertarget — tell me yours first.

🧠 Test yourself

Think it clicked? Take the 7-question quiz →

Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.

📚 Read next


🚀 Want more like this? Every guide, playground, and quiz lives on bestpractic.org — open it and sign up free so the next one finds you.

Thanks for reading! Let's stay connected:

Top comments (0)