Tabs Pattern for Switching Table and Chart Views

Permalink to "Tabs Pattern for Switching Table and Chart Views"

“Table” and “Chart” buttons above a data visualisation are one of the most common view switchers in data products — and one of the best accessibility features a chart can have, because the table view gives screen reader users full access to the numbers. Implemented as two loose buttons, though, they leave users unsure which view is active and what changed. The ARIA tabs pattern gives the switcher a clear structure: a list of tabs, one selected, each controlling a labelled panel.

This guide belongs to tabs & view switchers for data views.

Spec reference

Permalink to "Spec reference"
  • WAI-ARIA tabs pattern — role="tablist" containing role="tab" elements with aria-selected and aria-controls; each role="tabpanel" has aria-labelledby pointing at its tab. Only the selected tab is in the tab order; Left and Right arrows move between tabs; Home and End go to first and last.
  • Activation — automatic (arrow moves and activates) or manual (arrow moves; Enter or Space activates). Manual activation is recommended when showing a panel is slow.
  • SC 4.1.2 Name, Role, Value (A) — selected state exposed.
  • SC 1.3.1 Info and Relationships (A) — panel labelled by its tab.
  • SC 2.4.3 Focus Order (A) — Tab from the selected tab moves into its panel.
Switching from chart to table Sequence between the tablist, the chart panel and the table panel: Right arrow moves to the Table tab and activates it; the chart panel hides and the table panel shows; Tab moves into the table. Switching from chart to tableTablistChart panelTable panelRight arrow: focus "Table" tabactivate: show table panelhide chart panelTab: into the table
One tab stop for the whole tablist; Tab enters the active panel.

When to use — and when not to

Permalink to "When to use — and when not to"

Use tabs when several views of the same data, or closely related data, share one area and only one is visible at a time: table and chart, chart and summary, monthly and quarterly breakdowns.

Do not use tabs for navigation between pages — that is a navigation landmark with links, even if it looks like tabs. Do not use tabs when users need to compare views side by side; show both.

The misapplication: two toggle buttons with aria-pressed, one per view, styled as tabs. That works, but users get no “1 of 2” structure, both buttons are tab stops, and the panel has no name. Tabs communicate the relationship better.

Loose toggle buttons versus a tablist Comparison: two pressed-state buttons give no structure or panel label; a tablist exposes position, selection and a labelled panel. Loose toggle buttons versus a tablist✗ Two aria-pressed buttons"Table, toggle button, not pressed"Two tab stopsPanel unnamedRelationship implied by layout✓ Tablist with two tabs"Table, tab, 2 of 2, selected"One tab stop; arrows between tabsPanel labelled "Table"aria-controls and labelledby

Annotated code example

Permalink to "Annotated code example"
<div role="tablist" aria-label="Revenue by region views">        <!-- SC 1.3.1 -->
  <button role="tab" id="tab-chart" aria-selected="true" aria-controls="panel-chart" tabindex="0">Chart</button>
  <button role="tab" id="tab-table" aria-selected="false" aria-controls="panel-table" tabindex="-1">Table</button>
</div>
<div role="tabpanel" id="panel-chart" aria-labelledby="tab-chart" tabindex="0">
  <svg viewBox="0 0 20 20" role="img" aria-label="Revenue by region, bar chart; North highest at 4.2 million">…</svg>
</div>
<div role="tabpanel" id="panel-table" aria-labelledby="tab-table" hidden>
  <table>…</table>
</div>
const tabs = [...tablist.querySelectorAll('[role="tab"]')];
function activate(tab) {
  tabs.forEach(t => {
    const on = t === tab;
    t.setAttribute('aria-selected', String(on));             // SC 4.1.2
    t.tabIndex = on ? 0 : -1;
    document.getElementById(t.getAttribute('aria-controls')).hidden = !on;
  });
  tab.focus();
  saveView(tab.id);                                           // remember the preference
}
tablist.addEventListener('keydown', (e) => {
  const i = tabs.indexOf(document.activeElement);
  const next = { ArrowRight: i + 1, ArrowLeft: i - 1, Home: 0, End: tabs.length - 1 }[e.key];
  if (next === undefined) return;
  e.preventDefault();
  activate(tabs[(next + tabs.length) % tabs.length]);        // automatic activation: views are cheap
});
tablist.addEventListener('click', (e) => { const t = e.target.closest('[role="tab"]'); if (t) activate(t); });

Keyboard & AT behaviour

Permalink to "Keyboard & AT behaviour"
Key Action Expected announcement AT deviations
Tab into tablist Focus selected tab “Revenue by region views, tab list, Chart, tab, selected, 1 of 2” —
Right arrow Next tab, activated “Table, tab, selected, 2 of 2” —
Home / End First / last tab Tab name —
Tab Into the active panel Panel content or “Table, tab panel” VoiceOver may announce the panel name
Shift+Tab from panel Back to the selected tab Tab name —
Automatic versus manual activation Matrix of activation modes against panel load cost, accidental activation, number of key presses and recommended use. Automatic versus manual activationPanel costAccidentalactivationKey pressesUse whenAutomaticEvery arrow renders apanelPossibleOnePanels are instantManualOnly on EnterUnlikelyTwoPanels load data orare heavy
Choose manual when showing a panel is slow or expensive.

Integration context

Permalink to "Integration context"

The table view is the accessible alternative for the chart, as described in show-as-table toggles for charts, which also covers a simpler disclosure variant. If panels load data, see lazy-loaded tab panels with data. Telling users about the switch is covered in announcing view changes in data apps.

Gotchas

Permalink to "Gotchas"

Hidden panels keep state. If you unmount the table when the chart is shown, the user loses scroll position and sort. Prefer hidden over unmounting for light panels, or persist state.

Focusable panels. If a panel has no focusable content (a chart image), give it tabindex="0" so Tab can reach it; if it does, do not.

Tabs inside a grid toolbar. A tablist inside a toolbar inherits the toolbar’s arrow keys; keep tabs outside toolbars, or follow the toolbar’s model and drop the tab roles.

Framework notes

Permalink to "Framework notes"

React Aria Tabs, Radix Tabs, Headless UI Tab and Angular Material mat-tab-group all implement the pattern. Check two settings: activation mode (Radix activationMode="manual", React Aria keyboardActivation="manual") and whether inactive panels are unmounted (Radix forceMount, Angular preserveContent).

<Tabs.Root defaultValue="chart" activationMode="automatic">
  <Tabs.List aria-label="Revenue by region views">
    <Tabs.Trigger value="chart">Chart</Tabs.Trigger><Tabs.Trigger value="table">Table</Tabs.Trigger>
  </Tabs.List>
  <Tabs.Content value="chart" forceMount hidden={view !== 'chart'}>…</Tabs.Content>
  <Tabs.Content value="table" forceMount hidden={view !== 'table'}>…</Tabs.Content>
</Tabs.Root>

Worked example

Permalink to "Worked example"

A revenue dashboard had “Chart” and “Table” pills implemented as two links that reloaded the page with ?view=table. Screen reader users heard “link, Table” with no sense of which view was current, and every switch reloaded the page and lost scroll. Rebuilding as a tablist with hidden (not unmounted) panels made the current view audible (“Table, tab, selected”), removed the reload, and kept the table’s sort when switching back. The team kept the URL parameter in sync with replaceState so shared links still open the right view.

Testing checklist

Permalink to "Testing checklist"

Design system notes

Permalink to "Design system notes"

Provide a ViewTabs component preconfigured for data views — automatic activation for cheap panels, manual for loaders, panels preserved by default — and a convention that every chart offers a Table tab.

FAQ

Permalink to "FAQ"
Should "Table" and "Chart" be tabs or a toggle?

Tabs when the views share an area and users switch occasionally; a single “Show as table” toggle is fine too. Pick one model for the whole product.

Should the selected view persist across visits?

Yes. Users who rely on the table view should not have to switch every time. Store the preference per chart or globally.

Can tabs be links?

If each view has its own URL and switching is navigation, use links in a nav. If switching happens in place, use tabs. Do not mix the two semantics.

Does every panel need to be focusable?

Only if it has no focusable content; otherwise Tab already lands inside it.

How many tabs are too many?

More than five or six becomes hard to scan by ear. Consider a select or a menu for many views.

Should the table tab come first or second?

Many teams put the chart first because it is the visual default, which is fine. If your users are mostly analysing values, putting Table first is reasonable too. What matters is that the order is the same on every chart in the product.

Permalink to "Related"

← Back to Tabs & View Switchers for Data Views