Accessible accordions, tabs and disclosure widgets

Use the native details and summary elements for simple show and hide, or a real button whose aria-expanded switches between true and false. An accordion puts each such button inside a heading. Tabs are a different pattern, for panels that replace one another: they need the tablist, tab and tabpanel roles, one tab stop and arrow-key navigation.

Published . Last reviewed .

Accordions, tabs and disclosure widgets all hide content behind a label, so they are easy to confuse, but they promise different behavior to keyboard and screen reader users. W3C's ARIA Authoring Practices Guide (APG) describes a disclosure as a button that shows or hides one section of content, an accordion as a vertical stack of headings that each control a section, and tabs as layered panels of which one displays at a time. Each must tell assistive technology its name, role and current state, which is what success criterion 4.1.2 Name, Role, Value (Level A) asks for. W3C's note on it lists whether an accordion is expanded or collapsed as a state to expose.

That note also says standard HTML controls already meet 4.1.2 when used according to specification, so, in our reading, the order of choice is native elements first, ARIA patterns second, and a test with a keyboard and a screen reader last. 1.3.1 Info and Relationships (Level A) matters too: it asks that structure conveyed through presentation, such as which text is a section heading, can be programmatically determined or is available in text. It is one part of our guide on how to make a website accessible.

Accordion, tabs or disclosure: which one do you need?

The three patterns differ in what they hide, what they are built from and how a keyboard user moves through them.

Three show-and-hide patterns: when to use each, what to build it with and the keys it uses
PatternUse it whenBuild it withKeys (from the APG)
DisclosureOne control shows or hides one section, such as an image descriptiondetails and summary, or a button with aria-expandedEnter or Space on the button
AccordionA stack of sections whose titles belong in the page's heading outlineA button inside a heading for each section, with aria-expandedEnter or Space on a header. Tab moves through every focusable element
TabsSections that replace one another in one place, one visible at a timeThe tablist, tab and tabpanel roles, plus scriptTab into the list, arrow keys between tabs, Tab again to the panel

The HTML Standard draws one firm line: tab widgets and menu widgets are not disclosure widgets, so building them from the details element is incorrect. Past that line the choice is our reading, not a rule. Use a disclosure for an optional detail, an accordion when sections are parallel and readers may want several open or may scan the titles, and tabs only when the panels are views of one subject and one at a time suits the reader. Content most readers need should not sit in a closed section or an inactive tab at all.

Start with details and summary

MDN describes the details element as a disclosure widget with a summary element as its label, open when the open attribute is present. Its Baseline note says it has been available across browsers since January 2020. It needs no script: the browser supplies the toggle and the open state, and MDN describes focusing the summary and pressing the space bar to open it.

<details>
  <summary>Delivery options</summary>
  <p>Standard and express delivery are described here.</p>
</details>

Giving several details elements the same name attribute makes them exclusive, so opening one closes the others without script. The HTML Standard asks authors to weigh whether that helps, because it can frustrate users who must open many items to find what they want or who want several open at once. MDN's Baseline note says some parts of the feature vary in support, so check its compatibility table for the name attribute.

Two limits matter. MDN's page on the summary element warns that the role given to a summary varies across browsers and that some give it a button role, which removes the roles of its children, so a heading inside a summary may not be treated as a heading by screen readers. MDN also lists no permitted ARIA roles for details or summary, so adding a role attribute is not a way to fix them.

Our reading: use details and summary for plain show and hide, such as an answer in a list of questions, where the title need not appear in the heading list that screen reader users jump through (see how screen readers read a page). When each title should be a real heading, build the accordion from buttons.

How to build an accordion with buttons

The APG accordion pattern sets out what each section needs:

  • A button for the title. A native button is the simplest way to get the role, and it brings focus and the Enter and Space keys along. The APG's Read Me First page says ARIA roles do not make browsers provide keyboard behavior.
  • A heading around the button, at a level that fits the page's outline. The button is the only element inside the heading.
  • The state on the button. aria-expanded is true when the panel is visible and false when it is not.
  • A pointer to the panel. aria-controls holds the panel's ID.
  • Optionally, a region. The panel may have role region and aria-labelledby pointing at the button, but the APG advises against it where it would multiply landmarks, for example with more than approximately six panels open at once.

Enter or Space on a header opens a closed panel and closes an open one if collapsing is allowed. Tab and Shift+Tab move through every focusable element, headers and panel content alike. The pattern lists no arrow-key behavior.

<h3>
  <button type="button" id="header-1" aria-expanded="false" aria-controls="panel-1">Delivery options</button>
</h3>
<div id="panel-1" role="region" aria-labelledby="header-1" hidden>
  <p>Standard and express delivery are described here.</p>
</div>
const button = document.querySelector("#header-1");
const panel = document.querySelector("#panel-1");

button.addEventListener("click", () => {
  const open = button.getAttribute("aria-expanded") === "true";
  button.setAttribute("aria-expanded", String(!open));
  panel.hidden = open;
});

The script's job is to change aria-expanded and the panel's visibility together, so what a screen reader announces matches the screen. For the closed state use the hidden attribute: MDN says content marked with it is hidden from every presentation, including screen readers. In our reading, a panel only moved off screen or squeezed to no height may still be read or focused. MDN also documents hidden="until-found", which keeps collapsed content reachable by Find in page; check browser support before relying on it. For content hidden from screen readers that Tab can still reach, see aria-hidden and focusable elements.

In our reading, make the whole header row the button so the target is large (see touch target size). A chevron or plus sign that is the visual cue for open or closed is visual information that identifies a state, so under 1.4.11 Non-text Contrast it needs a contrast ratio of at least 3:1 against adjacent colors. W3C's Understanding note names an arrow graphic showing that a menu is open as an example.

How to build accessible tabs

The APG tabs pattern uses three roles joined by attributes: a tablist container, the tabs inside it and a tabpanel for each tab.

Roles, states and properties in the APG tabs pattern
AttributeOnWhat it does
role="tablist"The container of the tabsGroups the tabs. Labeled by aria-labelledby pointing at a visible label, or by aria-label when there is none
role="tab"Each tabSits inside the tablist. aria-controls points at its panel
aria-selectedEach tabtrue on the active tab and false on all the others
role="tabpanel"Each panelaria-labelledby points at its tab, which names the panel
aria-orientation="vertical"The tablistOnly when the tabs are stacked vertically. Horizontal is the default

The keys, from the same pattern:

  • Tab moves focus onto the active tab. Tab again leaves the list for the panel, unless the panel's first meaningful content is focusable.
  • Left Arrow and Right Arrow move to the previous or next tab, wrapping at the ends. A vertical tab list uses Up Arrow and Down Arrow. Home and End, for the first and last tab, are optional.
  • Space or Enter activates a tab that was not activated when it received focus.

The APG recommends automatic activation, where a tab opens its panel as soon as it gets focus, only when panels display without noticeable latency, which typically means their content is already loaded. Otherwise it warns that automatic activation slows focus movement, and manual activation, where Space or Enter opens the panel, is the alternative.

Only one tab is in the page's Tab sequence. The APG's guide to developing a keyboard interface calls the technique a roving tabindex: the element that should take focus has tabindex="0", the others tabindex="-1", and script moves the 0 as arrow keys move focus. In its automatic-activation example the tabs are buttons, so the selected tab needs no tabindex. Each panel has tabindex="0", which the pattern recommends when any panel starts with content that is not focusable.

<div role="tablist" aria-label="Product information">
  <button type="button" role="tab" id="tab-1" aria-selected="true" aria-controls="tabpanel-1">Description</button>
  <button type="button" role="tab" id="tab-2" aria-selected="false" aria-controls="tabpanel-2" tabindex="-1">Specifications</button>
</div>
<div role="tabpanel" id="tabpanel-1" aria-labelledby="tab-1" tabindex="0">...</div>
<div role="tabpanel" id="tabpanel-2" aria-labelledby="tab-2" tabindex="0" hidden>...</div>

Selected and focused are different tab states. The APG example marks the active tab with thicker borders and draws the focus ring separately, so people on high contrast settings can tell the selected tab from the focused one. See focus indicators for the focus side.

Tabs are for panels, not pages. In our reading, if choosing a "tab" loads another URL, those are links in a navigation list and should be marked up that way (see buttons vs links).

Mistakes to look for

  • A div or span as the toggle. A div or span with only a click handler cannot take focus from the keyboard, so it fails 2.1.1 Keyboard (Level A), which requires all functionality to be operable through a keyboard interface. Use a button.
  • State that never updates, or sits on the wrong element. aria-expanded belongs on the header button and aria-selected on each tab. 4.1.2 asks for notification of changes.
  • Every tab in the Tab sequence. The APG says the sequence should include only one focusable element of a composite component.
  • An icon-only toggle with no name. A button holding only a plus icon has no accessible name unless the icon or the button supplies one. The free scan flags a button with no accessible name under 4.1.2.
  • ARIA for its own sake. The APG's Read Me First page is headed "No ARIA is better than Bad ARIA", and it explains that some ARIA covers up or overrides the semantics an element already has.

Related widgets have their own guides: accessible modals for content that opens over the page and takes focus, accessible menus, accessible carousels and accessible forms. ARIA basics explains the attributes used here.

Check it yourself

This five-step routine is ours, built on the APG patterns above. It extends our keyboard accessibility test.

  1. Tab to every header or tab set. You should always see where focus is, and every accordion header should be reachable.
  2. Press Enter, then Space, on a header. The panel should open and close, and focus should stay on the header.
  3. In a tab set, press Tab once. Focus should land on the active tab. Right Arrow should move to the next tab, and Tab should leave the list for the panel. If Tab steps through every tab, the roving tabindex is missing.
  4. Close a panel or switch tabs, then keep pressing Tab. Focus should never land on something you cannot see.
  5. Listen with a screen reader. Each header should give its name, role and expanded or collapsed state, and each tab its selected state. Results differ between browser and screen reader pairs, so test the ones your visitors use.

The free accessibility bookmarklet checks the page as your browser has rendered it, so open the panels you care about first and run it again after each change. It lists the heading outline, which shows whether accordion titles are headings, flags buttons with no accessible name such as an icon-only toggle, and measures the contrast of rendered text. It does not check aria-expanded, tab roles or whether the keys work. The heading checker draws the outline from a page address or pasted HTML; paste the HTML from your browser's inspector to include headings a script adds.

Scan my site for free runs rule-based checks on the HTML your server sends, including buttons with no accessible name. It does not open a collapsed panel or press a key. What we check lists every rule. Passing these checks is a starting point, not proof that a page meets WCAG.

Accordion, tabs and disclosure questions

Should I use the details element or build my own accordion?

Use details and summary for plain show and hide, such as an FAQ answer, because the browser supplies the state and keyboard behavior with no script. Build your own with a button inside a heading when each title should be a heading in the page outline, since MDN warns that browsers differ in how they expose a heading inside a summary. Test whichever you choose with a screen reader.

What ARIA does an accordion need?

The APG accordion pattern uses a button for each header, wrapped in a heading, with aria-expanded set to true when the panel is open and false when it is closed, and aria-controls pointing at the panel's ID. A role of region with aria-labelledby on the panel is optional, and best avoided when more than about six panels can be open at once.

Which keys should tabs support?

In the APG tabs pattern, Tab moves focus onto the active tab and then out to the panel, and Left and Right Arrow move between tabs, wrapping at the ends. Space or Enter activates a tab that was not activated on focus. Home and End, to jump to the first and last tab, are optional. A vertical tab list uses Up and Down Arrow instead.

Should I use tabs or an accordion?

Use tabs when the panels are views of one subject and a reader needs one at a time. Use an accordion when readers may want several sections open together or scan the titles as headings. That is our reading, not a rule, with one fixed point: the HTML Standard says tab widgets are not disclosure widgets, so tabs should not be built from details elements.

Should collapsed accordion content be hidden from screen readers?

Yes. A closed panel is not visible, so its content should be out of the screen reader's view and out of the tab order as well. MDN says content with the hidden attribute is hidden from every presentation, including screen readers. In our reading, a panel only moved off screen or squeezed to no height may still be read or focused, so hide it properly.

Does the name attribute on details make an accessible accordion?

Not by itself. The name attribute makes grouped details elements exclusive, so opening one closes the others without script. The HTML Standard asks authors to weigh that choice, because it can frustrate users who must open many items to find something, or who want several open at once. In our reading, letting people open more than one section is the safer default.

Is aria-controls required on an accordion button?

In the APG accordion pattern, yes: the header button has aria-controls set to the panel's ID. In the APG disclosure pattern it is optional. Either way, the parts that matter most are a real button, an aria-expanded state that changes with the panel, and a test in the browsers and assistive technology your audience uses.

Run the automated part first

The free scan reads the HTML your server sends and flags buttons with no accessible name, among its rule-based checks. It cannot open a panel or press keys, so use the test above for the rest. No signup.

Scan my site for free