Tabs & View Switchers for Data Views

Permalink to "Tabs & View Switchers for Data Views"

Data views are rarely shown one way. The same data appears as a table or a chart, a list or a board, grouped or flat, compact or spacious, filtered into one saved view or another. View switchers — tabs, segmented controls, dropdowns of saved views — are therefore among the most-used controls in data apps. They are also deceptively easy to get wrong: buttons styled as tabs with no tab semantics, icon-only segments with no names, lazy panels that fire requests on every arrow press, and view changes that redraw the whole screen without a word to screen reader users.

The users most affected are screen reader users, who need to know which view is active and what it contains; keyboard users, who need predictable arrow-key models and no accidental loads; low-vision users, who depend on comfortable density and persistent preferences; and users with cognitive disabilities, for whom silent, sweeping changes are disorienting. The failure this topic prevents is a user who switched views and does not know what they are now looking at.

This topic belongs to core ARIA & keyboard navigation for data UIs.

WCAG criteria in scope

Permalink to "WCAG criteria in scope"
Criterion Level Relevance to view switchers
1.3.1 Info and Relationships A Tabs, groups and panels related programmatically
1.4.1 Use of Color A Selected segment and “modified” states not colour-only
1.4.12 Text Spacing AA Densities survive text-spacing overrides
2.4.2 Page Titled A Major view changes update the title
2.5.8 Target Size (Minimum) AA Compact densities keep targets usable
3.2.1 On Focus A Focus on a tab does not trigger costly loads
4.1.2 Name, Role, Value A Selected tab, checked radio, current view exposed
4.1.3 Status Messages AA View changes summarised

Prerequisites

Permalink to "Prerequisites"
Which semantics for which switcher Matrix of switchers — table and chart toggle, list and board, density, units or period, saved views — against tablist, radio group and menu semantics. Which semantics for which switcherTablistRadio groupMenuTable / chartYesNoNoList / boardYesNoNoDensityNoYesNoUnits or periodNoYesIf many optionsSaved viewsNoNoYes
Panels mean tabs; values mean radios; named configurations mean menus.

ARIA & HTML spec reference

Permalink to "ARIA & HTML spec reference"
Element / attribute Valid values When to apply Common misuse
role="tablist" / tab / tabpanel — Switchers that show one of several panels Page navigation styled as tabs
aria-selected true / false Tabs aria-pressed on tabs
aria-controls Panel ID Tab to panel Pointing at a panel that does not exist yet
fieldset + legend / radiogroup — Density, units, period Toggle buttons for mutually exclusive values
menuitemradio + aria-checked — Saved views in a menu Plain menuitems with a visual tick
aria-busy true / false Lazy panels loading Never cleared on error
role="status" — View summaries Echoing the control’s own name

Step-by-step implementation

Permalink to "Step-by-step implementation"

Step 1 — Pick semantics (SC 1.3.1, 4.1.2)

Permalink to "Step 1 — Pick semantics (SC 1.3.1, 4.1.2)"

Tablist for panels, radio group for values, menu for saved configurations.

<div role="tablist" aria-label="Revenue views">…</div>
<fieldset><legend>Row density</legend>…</fieldset>

Step 2 — One tab stop, arrows inside

Permalink to "Step 2 — One tab stop, arrows inside"
tabs.forEach(t => (t.tabIndex = t.getAttribute('aria-selected') === 'true' ? 0 : -1));

See the tabs pattern for switching table and chart views.

Step 3 — Manual activation for lazy panels (SC 3.2.1)

Permalink to "Step 3 — Manual activation for lazy panels (SC 3.2.1)"
if (e.key === 'Enter' || e.key === ' ') activate(focusedTab);   // arrows only move

See lazy-loaded tab panels with data.

Step 4 — Summaries, not echoes (SC 4.1.3)

Permalink to "Step 4 — Summaries, not echoes (SC 4.1.3)"
status.textContent = `${view.cardCount} cards in ${view.columns.length} columns.`;

See announcing view changes in data apps.

Step 5 — Saved views with clear state (SC 1.4.1, 4.1.2)

Permalink to "Step 5 — Saved views with clear state (SC 1.4.1, 4.1.2)"
<button aria-haspopup="menu">View: My overdue invoices <span>(modified)</span></button>

See saved views menus for data grids.

Step 6 — Safe densities (SC 2.5.8, 1.4.12)

Permalink to "Step 6 — Safe densities (SC 2.5.8, 1.4.12)"
.grid[data-density="compact"] input[type="checkbox"] { inline-size: 24px; block-size: 24px; }

See segmented controls for view density.

Choosing the switcher Flow: does the control show a different panel? Tablist. Does it set a value such as density? Radio group. Does it apply a named configuration? Menu. Will panels load data? Use manual activation. Choosing the switcherShows a different panel?table, chart, boardtablistSets a value?density, units, periodradio groupApplies a named configuration?saved viewsmenu with menuitemradioPanels load data?separate requestsmanual activation

Keyboard interaction contract

Permalink to "Keyboard interaction contract"
Switcher Key Action Expected announcement Failure indicator
Tablist Arrows Move (and activate if automatic) “Table, tab, selected, 2 of 2” Each tab is a Tab stop
Tablist Enter / Space Activate (manual) Tab selected, then summary Arrowing triggers loads
Tablist Tab Into active panel Panel content Skips the panel
Radio group Arrows Select option “Compact, radio button, checked, 1 of 3” Icons with no names
Views menu Enter Open “View: …, menu” —
Views menu Enter on a view Apply; focus to button Summary of the new view Silent switch

Screen reader compatibility matrix

Permalink to "Screen reader compatibility matrix"
AT + browser Tabs Radio group menuitemradio Notes
NVDA + Chrome Position and selection Group label and position “checked” Reliable
JAWS + Chrome Same Same “checked” Tab panels named on entry
VoiceOver + Safari “tab, selected, 2 of 2” “radio button, 1 of 3” “ticked” Hidden native radios must not use display: none
TalkBack + Chrome “tab, selected” “radio button, checked” Read Swipe navigation through tabs
Narrator + Edge Read Read Read —

Edge cases & failure modes

Permalink to "Edge cases & failure modes" Permalink to "1. Tabs that are links"

Diagnosis: page navigation marked as tabs, or tabs that reload the page. Fix: links in a nav for navigation; tabs only for in-place panels.

2. Lazy panels with automatic activation

Permalink to "2. Lazy panels with automatic activation"

Diagnosis: arrowing fires requests and announcements. Fix: manual activation.

3. Echo announcements

Permalink to "3. Echo announcements"

Diagnosis: “Board view” after “Board, tab, selected”. Fix: announce content only.

4. Lost state on switch

Permalink to "4. Lost state on switch"

Diagnosis: the table’s sort resets each time the chart is viewed. Fix: hide panels instead of unmounting, or persist state.

5. Compact density fails target size

Permalink to "5. Compact density fails target size"

Diagnosis: 16-pixel checkboxes in compact rows. Fix: keep 24-pixel targets via padding.

Tabs pattern for switching table and chart views

Permalink to "Tabs pattern for switching table and chart views"

A labelled tablist with one tab stop, arrows between tabs and panels labelled by their tabs; preserve each panel’s state. See the tabs pattern for switching table and chart views.

<div role="tabpanel" id="panel-table" aria-labelledby="tab-table" hidden>…</div>

Segmented controls for view density

Permalink to "Segmented controls for view density"

A radio group with text labels, non-colour selection and densities that keep targets and text spacing safe. See segmented controls for view density.

<label><input type="radio" name="density" value="compact"> Compact</label>

Saved views menus for data grids

Permalink to "Saved views menus for data grids"

Current view and “(modified)” in the button name, views as menuitemradio in labelled groups, diff-based announcements on switch. See saved views menus for data grids.

status.textContent = `View: ${view.name}. ${rows} rows.`;

Announcing view changes in data apps

Permalink to "Announcing view changes in data apps"

Announce what the control does not: counts, groupings and hidden columns — after user-initiated switches only. See announcing view changes in data apps.

if (source === 'user') status.textContent = summary(view);

Lazy-loaded tab panels with data

Permalink to "Lazy-loaded tab panels with data"

Manual activation, busy panels with visible loading text, a threshold before announcing “Loading”, errors inside the panel and caching. See lazy-loaded tab panels with data.

panel.setAttribute('aria-busy', 'true'); /* … */ panel.setAttribute('aria-busy', 'false');

Cross-cutting concerns

Permalink to "Cross-cutting concerns"

Preferences. Users who rely on the table view, spacious density or a particular saved view should get it by default next time. Store view preferences per user and per surface, and apply them before first render to avoid announcing a switch on load.

URLs. Views that users might share or return to belong in the URL. Use replaceState for minor switches (density) and pushState for major ones (a different report), so Back behaves as users expect.

Consistency. A product that uses tabs for table/chart in one place, toggle buttons in another and a dropdown in a third forces users to relearn the same task. Choose one switcher per kind of switch across the product.

Responsive layouts. On narrow screens, tablists often become selects. That is fine — a select is accessible — as long as the switch still announces its summary and preserves state.

A user's view preference across a session Chain: the user picks the Table tab on a chart; the preference is stored; the next chart page opens in Table view without any announcement; switching back to Chart there is announced with a summary. A user's view preference across a sessionChoose Tableon the revenue chartPreference"prefer table" forchartsOpens in Tableno announcement onloadChart selectedsummary announcedstore preferencenext pageuser switches
Preferences apply silently; user switches are announced.

Worked example: a reporting module

Permalink to "Worked example: a reporting module"

A reporting module offered six report types, each with Chart, Table and Summary views, a density toggle and saved views. An audit with a screen reader user found that the view pills were links that reloaded the page, the density control was three unlabelled icons, the saved views dropdown switched silently, and the Summary panel fetched data on every arrow press. The fixes: a tablist with manual activation for the three views, a labelled radio group for density, a menu with menuitemradio and announced summaries for saved views, and a stored preference that opens every report in the view the user last chose. The user’s own feedback: “Now I always know where I am.”

Who is affected, and how

Permalink to "Who is affected, and how"
User group Typical failure Fix
Screen reader users Unknown current view; silent switches Selected state; content summaries
Keyboard users Many tab stops; accidental loads One tab stop; manual activation
Low-vision users Compact defaults; lost preferences Safe densities; stored preferences
Voice control users Icon-only segments cannot be named Visible text labels
Users with cognitive disabilities Sweeping silent changes Consistent switchers; summaries

Common mistakes in component libraries

Permalink to "Common mistakes in component libraries"

Most teams use a component library’s tabs and toggle groups, and most libraries implement the patterns correctly in isolation. The mistakes come from how they are configured and combined:

  • Toggle groups used as tabs. Radix ToggleGroup, MUI ToggleButtonGroup and similar components expose pressed buttons, not tabs. They are fine for density or units, wrong for switching panels.
  • Unmounting inactive panels. Default settings in several libraries unmount hidden tab content, discarding the table’s sort and scroll every time users look at the chart.
  • Automatic activation with data fetching. Libraries default to automatic activation; combined with data hooks keyed on the selected tab, every arrow press fetches.
  • Icon-only segments. Libraries render whatever children you pass. An icon with no text produces an unnamed control unless you add a label.
  • Menus for single choices without checked state. A plain menuitem with a visual tick does not tell screen readers which view is active; use menuitemradio.

Each of these is a one-line fix — an activationMode, a forceMount, an aria-label or a different component — once you know to look for it.

Measuring view-switcher quality

Permalink to "Measuring view-switcher quality"

Two simple checks catch most problems in an existing product. First, count tab stops: Tab through the page header and toolbars and note how many stops each switcher takes; anything above one per switcher is a smell. Second, listen to five view switches with a screen reader and note whether you heard both the selected view and what it contains. Track both after any change to shared components.

Design system integration

Permalink to "Design system integration"

Provide ViewTabs, DensityControl and SavedViewsMenu as distinct components with the semantics in this topic, plus a view registry where each view declares its summary(), its loader (if any) and whether it is major. The switcher components then handle activation mode, announcements, URL updates and preference storage consistently. Density tokens must be verified against 2.5.8 and 1.4.12 for every density offered.

Testing checklist

Permalink to "Testing checklist"

Automated

Permalink to "Automated"

Keyboard

Permalink to "Keyboard"

Screen reader

Permalink to "Screen reader"

FAQ

Permalink to "FAQ"
Should view switchers be tabs or buttons?

Tabs when they control panels in place; radio groups when they set a value; menus for saved views. Plain buttons with aria-pressed work for a single on/off toggle.

Automatic or manual tab activation?

Automatic for instant panels; manual when panels are slow or costly.

How do I make the table view the default for screen reader users?

Do not detect screen readers. Let every user choose, remember the choice, and make the table view easy to find.

Should switching to the chart view announce the chart's description?

A short summary, yes (“Bar chart of 8 regions”); the full description belongs in the chart’s own text alternative.

What about view switchers inside toolbars?

A toolbar has its own arrow-key model. Put tablists outside toolbars, or use toggle buttons inside them following the toolbar pattern.

How do I test view switching quickly?

With a screen reader on, switch each view with the keyboard and ask: did I hear which view is selected, and what it contains? Then reload the page: did anything get announced that should not have been?

Can a select element replace a tablist?

Yes, especially on small screens. Pair it with the same summary announcement after change.

Do view switchers need visible labels?

Yes, ideally. Visible text helps everyone recognise the options, lets voice control users say them, and makes the accessible name obvious. Icons can accompany the text.

Permalink to "Related"

← Back to Core ARIA & Keyboard Navigation for Data UIs