# Tabs that only work with a mouse

Source: https://easeweb.dev/learn/tabs
Topics: Tabs, accordions and disclosures > Tabs follow the tabs pattern (WCAG 4.1.2, 2.1.1)
The fix: Build the tabs pattern on buttons, by hand or with a tested tabs component: role="tab" in a role="tablist", aria-selected on the shown tab, one Tab stop, and the arrow keys, Home and End to move between tabs. Keep every panel in the page and hide the others with hidden.
Test with: keyboard, screen reader, zoom, forced colors
References: https://www.w3.org/WAI/ARIA/apg/patterns/tabs/ https://www.w3.org/WAI/ARIA/apg/patterns/tabs/examples/tabs-automatic/ https://www.w3.org/WAI/WCAG22/Understanding/name-role-value.html https://www.w3.org/WAI/WCAG22/Understanding/keyboard.html

This is a recreated teaching example, not a finding about a named client. The code is a starting pattern: test it in your own product. No completed assistive-technology test is claimed here.

## What happens

A product page for a jacket has three tabs under the photo: Description, Sizes and Reviews (12). Click Sizes and the size table replaces the description. Press Tab instead and focus jumps over all three tabs, straight to the Care instructions link inside the description. There is no key that opens Sizes or Reviews. A screen reader user reading the page hears a list of three words, then the description, and nothing to suggest that two more panels are waiting behind the other words.

## Why it fails

Each tab is an `li` with a click handler. A list item is not a control: it takes no focus, answers no keys, and has no role a screen reader can announce. The click handler only works for a pointer, so the panels behind it fail WCAG 2.1.1 Keyboard. The selected tab is shown by an `active` class, which only the stylesheet reads, so the tab's role and state fail 4.1.2 Name, Role, Value: nothing in the code says that these words are tabs, that they control the panels below, or which one is selected.

## Who is affected

Product pages, account settings and pricing tables often put the details people came for behind the second or third tab. When the tabs work only with a mouse, the people they shut out lose the content, not just a shortcut to it.

## What automation and AI miss

A probe that compares click handlers with the Tab order can find list items that react to clicks but never take focus. It can't tell whether the selected state is exposed, whether that state moves when another tab is chosen, or whether the arrow keys work. AI code generators often add `role="tab"` and `aria-selected` to the same list items and stop there. That is not a fix: a screen reader now says "tab", but the items still can't be reached with Tab or opened with a key, so the person is told about a control they can't use. Another common patch renders only the shown panel, which leaves `aria-controls` on the other tabs pointing at ids that don't exist. Pressing Tab, the arrow keys and Enter on every tab, then listening to each one, is the only way to confirm the pattern works.

## Before: list items with click handlers

```html
<ul class="tabs">
  <li class="tab active" onclick="show('description')">Description</li>
  <li class="tab" onclick="show('sizes')">Sizes</li>
  <li class="tab" onclick="show('reviews')">Reviews (12)</li>
</ul>
<div class="panel" id="description">
  <p>Waxed cotton, lined with wool. <a href="/care">Care instructions</a></p>
</div>
<div class="panel" id="sizes" style="display: none">…</div>
<div class="panel" id="reviews" style="display: none">…</div>
```

The list items are never in the Tab order, so the keyboard can't open Sizes or Reviews. The `active` class shows the selected tab to the eye only.

## After: the tabs pattern on buttons

```html
<div role="tablist" aria-label="Product details">
  <button type="button" role="tab" id="tab-description"
          aria-selected="true" aria-controls="panel-description">Description</button>
  <button type="button" role="tab" id="tab-sizes"
          aria-selected="false" aria-controls="panel-sizes" tabindex="-1">Sizes</button>
  <button type="button" role="tab" id="tab-reviews"
          aria-selected="false" aria-controls="panel-reviews" tabindex="-1">Reviews (12)</button>
</div>
<div role="tabpanel" id="panel-description" aria-labelledby="tab-description" tabindex="0">
  <p>Waxed cotton, lined with wool. <a href="/care">Care instructions</a></p>
</div>
<div role="tabpanel" id="panel-sizes" aria-labelledby="tab-sizes" tabindex="0" hidden>…</div>
<div role="tabpanel" id="panel-reviews" aria-labelledby="tab-reviews" tabindex="0" hidden>…</div>
```

```js
const tabs = [...document.querySelectorAll('[role="tablist"] [role="tab"]')];

function select(tab) {
  for (const t of tabs) {
    const on = t === tab;
    t.setAttribute('aria-selected', String(on));
    t.tabIndex = on ? 0 : -1;
    document.getElementById(t.getAttribute('aria-controls')).hidden = !on;
  }
  tab.focus();
}

for (const tab of tabs) {
  tab.addEventListener('click', () => select(tab));
  tab.addEventListener('keydown', (event) => {
    const i = tabs.indexOf(tab);
    const to = { ArrowRight: i + 1, ArrowLeft: i - 1, Home: 0, End: tabs.length - 1 }[event.key];
    if (to === undefined) return;
    event.preventDefault();
    select(tabs[(to + tabs.length) % tabs.length]);
  });
}
```

```css
[role="tab"][aria-selected="true"] {
  font-weight: bold;
  border-bottom: 3px solid currentColor;
}
```

The tab list takes one Tab stop, on the selected tab, and the arrow keys move between tabs and show each one's panel as they go; Home and End jump to the ends. A screen reader announces something like "Sizes, tab, selected, 2 of 3". Every panel stays in the page, hidden, so `aria-controls` always points at a real element and the links in hidden panels take no focus. The panels have `tabindex="0"` because they start with text, so the next Tab after the tab list lands on the panel itself. The selected look is drawn from `aria-selected`, so it can't show one tab while the code marks another.

## Implementation decisions

A tested tabs component does all of this for you, and is usually safer than a hand-written script. Check that the one you use gives the tab list one Tab stop, moves with the arrow keys, sets `aria-selected`, and hides the other panels; some libraries style a row of buttons as tabs and stop there. The pattern still leaves several choices to you:

- Automatic or manual activation: the code above shows each panel as the arrow keys reach its tab. If a panel loads slowly, show it only on Enter or Space, and let the arrow keys move focus without selecting.
- Vertical tabs: add `aria-orientation="vertical"` to the tab list and use the Up and Down arrows instead.
- Panels that render on demand: frameworks often render only the selected panel. Render every panel and hide the others with `hidden`, or set `aria-controls` only on the selected tab, so it never points at nothing and an audit has nothing to report.
- Narrow screens: if the tabs turn into an accordion on a phone, switch the roles with the layout. A row of `role="tab"` elements stacked vertically is still announced as tabs.
- Panel focus: the panels here start with text, so they take `tabindex="0"`. A panel that starts with a link or a field needs no tabindex, since the next Tab lands on that control.

Tabs that each load a different page are not a tab widget at all. They are navigation: use links in a `nav`, and mark the current one with `aria-current="page"`. Keep the selected tab visible in forced colors with a border or weight, not a background colour alone.

## Verify the fix

1. Put the mouse aside. Tab to the tab list, move through every tab with the arrow keys, Home and End, and Tab on into the shown panel.
2. With a screen reader, move to each tab and confirm it is announced as a tab, with whether it is selected and its position, and that the announcement changes when you choose another.
3. Choose a tab, then Tab through the page and confirm nothing inside the hidden panels takes focus.
4. In the browser's inspector, confirm every `aria-controls` and `aria-labelledby` id points at an element in the page.
5. At 200% zoom and in forced colors, confirm every tab is visible and the selected one still stands out.

Record the OS, browser and assistive-technology versions, the build, the date, and the actual result of each step. Repeat after the shared tabs component changes.

## Limits

This is a starting pattern, not a guarantee for every application. The script is a minimal sketch: it handles one tab list per page, and has no deep links or remembered selection. Screen readers word tabs and their position differently. Test the complete journey with your target assistive technology.
