An accordion is a stack of headings that expand. A tab list is a row of choices where exactly one panel shows at a time. They solve different problems and their keyboard contracts have almost nothing in common, which is why picking the wrong one costs so much.
Teams reach for tabs because the word sounds more precise. Tabs are far harder to implement, and a half-built tab list behaves worse than a plain accordion ever would, because declaring the role is a promise about behavior that the browser will not keep for you.
The Accordion Is the Easy One
Each section header is a button. The button carries aria-expanded, true when its panel is open and false when it is closed, and aria-controls pointing at the panel it opens. Tab moves between headers. Enter and Space toggle. There is no arrow key behavior at all, and that absence is the whole reason this pattern is cheap.
Four details finish it, and three of them are the ones we see skipped.
- Wrap each button in a real heading element at whatever level fits the page's structure, so the accordion still makes sense in a screen reader's heading list. Skip this and you have flattened the page and quietly failed 1.3.1.
- The button must be the only thing inside that heading. This is the rule teams break without noticing, because the chevron, the count badge and the little menu button all want to live in the header too. Put them beside the heading rather than inside it, or the heading text turns into a run-on.
- If one panel always has to stay open, mark that header
aria-disabled="true"while its panel is showing, so somebody is not pressing a control that cannot do anything. role="region"on each panel is optional, and it is genuinely useful when panels hold headings or a nested accordion, because it gives a screen reader user something to jump to. It has a cap. Do not use it on an accordion with more than about six panels that can be open at once, or you have buried the page's real landmarks under a pile of new ones.
The details Element Is a Different Pattern, Not a Shortcut
Native details and summary give you a button, a state, and keyboard behavior with no JavaScript, and that is a real offer. It is also not the accordion pattern. It is the disclosure pattern, which is a related and simpler thing where aria-controls is optional rather than required and no heading wrapper is specified.
The reason that matters is the one piece of advice on this page it quietly cancels. The role browsers assign to summary varies, and some still give it a button role, which strips the roles off everything inside it. So the heading you carefully put in your summary is not a heading for those users, which is exactly the failure the section above told you to avoid. Use details for a simple show and hide where the header is not carrying structure. Test it across browsers before you use it for anything else.
Tabs Ask a Great Deal More of You
A tab list moves focus with arrow keys rather than with Tab. Tab enters the list and lands on the active tab. Left and right arrows move focus between tabs, wrapping from the last to the first. Tab again leaves the list, and where it goes next is the part most hand-built tab lists get wrong.
It lands in the open panel only if the panel's first meaningful content is focusable. A panel of plain text is skipped entirely, so the keyboard user tabs from the tab list straight past the content they just chose. The fix is tabindex="0" on the panel itself whenever it has no focusable content of its own, and it is the single most common tabs failure after the arrows.
Only one tab sits in the page's tab order at a time. The selected tab carries tabindex="0" and the rest carry tabindex="-1", and those values move as the selection moves. The technique is called a roving tabindex, and it is worth knowing the name because the same mechanism runs tab lists, toolbars, radio groups, grids and date pickers. Our tabindex entry covers what the values themselves do.
- The container takes
role="tablist", and it needs an accessible name. Pointaria-labelledbyat a visible label if there is one, and usearia-labelif there is not. - Each tab takes
role="tab", plusaria-controlspointing at its panel andaria-selected. The selected tab istrueand every other tab is explicitlyfalse, not absent. - Each panel takes
role="tabpanel"andaria-labelledbypointing back at its tab. - Arrow keys move focus. Whether selection follows focus is a decision you make, not a default. Activating on focus is recommended when the panels are already loaded and appear with no noticeable delay. Where a panel takes a moment to arrive, activate on Enter or Space instead, because automatic activation slows every arrow press down to the speed of the slowest panel.
- Home and End are optional, and worth adding. They jump to the first and last tab.
- A vertical tab list needs
aria-orientation="vertical", and then up and down do what left and right do elsewhere. A horizontal list must not listen for up and down at all, because those keys are how a keyboard user scrolls the page.
Get any of that wrong and you have a widget announcing itself as tabs while behaving like something else, which is worse than not claiming the role. The specification puts it as a requirement rather than a preference. An element with role="tab" must be inside an element with role="tablist". And the framing W3C uses for the whole family is the one to keep. A role is a promise, ARIA gives you no behavior for free, and no ARIA is better than bad ARIA. A mismatch between the announced role and the actual behavior is a 4.1.2 failure.
Choosing Between Them
| Situation | Use |
|---|---|
| Content people may want open at once | Accordion |
| Mutually exclusive views of the same thing | Tabs |
| Narrow screens | Accordion, which stacks naturally |
| Long content people mostly skip | Accordion |
| Fewer than three choices | Neither. Just show it |
The Failure Nobody Notices
Content hidden inside a collapsed panel is invisible to the browser's own find-in-page. Somebody presses Ctrl-F, types the thing they came for, and the browser reports not found on a page that contains it. That is not a WCAG failure on its own and it is a real way to lose a reader.
The platform has an answer now that we used to work around. hidden="until-found" keeps a panel visually hidden while leaving its content reachable by find-in-page and by a link to an anchor inside it, and the browser opens the panel when it matches. One limit to know before you rely on it. It does nothing if the element's display is none, contents, or inline, which rules out most of the ways a panel is usually hidden, so this is a change to how you hide rather than an attribute you sprinkle on top.
Before You Copy the Example Code
Every worked example in the ARIA Authoring Practices Guide carries the same warning, and it is worth reading once. The code is not intended for production. It illustrates correct use of the specification, and it deliberately does not work around gaps in browser and screen reader support, especially on mobile and touch devices. So the examples are the right thing to build from and the wrong thing to ship untested.