Segmented Controls for View Density
Permalink to "Segmented Controls for View Density"Data grids often offer density settings: compact rows for experts who want to see more data, comfortable or spacious rows for everyone else. The control is usually a “segmented control” — three joined buttons with icons. Accessible density control matters twice: the control itself must be operable and understandable, and the densities it produces must not break accessibility (compact rows that shrink targets below 24 pixels, or that clip text when users increase spacing).
This guide belongs to tabs & view switchers for data views.
Spec reference
Permalink to "Spec reference"- Radio group semantics — a set of mutually exclusive options is a radio group: native
input type="radio"in afieldsetwithlegend, orrole="radiogroup"withrole="radio"andaria-checked. Arrow keys move and select; only the checked radio is a tab stop. - SC 1.3.1 (A) and SC 4.1.2 (A) — group label and selected state are programmatic.
- SC 1.4.1 Use of Color (A) and SC 1.4.11 Non-text Contrast (AA) — the selected segment needs a non-colour indicator with 3:1 contrast.
- SC 2.5.8 Target Size (Minimum) (AA) — compact density must keep interactive targets at 24×24 CSS pixels or with sufficient spacing.
- SC 1.4.12 Text Spacing (AA) — rows must not clip text when users increase line height or letter spacing.
When to use — and when not to
Permalink to "When to use — and when not to"Offer density control when the grid is used both by power users scanning many rows and by occasional users, or when users with low vision need more space. Keep the choices to two or three.
Do not make compact density the default if it pushes targets below 24 pixels or text below a comfortable size. Do not implement density by scaling with transform or zoom — that shrinks text and targets together and can break reflow.
The misapplication: a compact mode that reduces row height to 24 pixels while keeping checkbox targets at 16 pixels with no spacing, and that clips descenders when users apply a text-spacing bookmarklet.
Annotated code example
Permalink to "Annotated code example"<fieldset class="density">
<legend>Row density</legend> <!-- group label (SC 1.3.1) -->
<label><input type="radio" name="density" value="compact"> <svg viewBox="0 0 20 20" aria-hidden="true">…</svg> Compact</label>
<label><input type="radio" name="density" value="comfortable" checked> <svg viewBox="0 0 20 20" aria-hidden="true">…</svg> Comfortable</label>
<label><input type="radio" name="density" value="spacious"> <svg viewBox="0 0 20 20" aria-hidden="true">…</svg> Spacious</label>
</fieldset>
/* Segmented look; native radios underneath (keyboard and semantics for free) */
.density input { position: absolute; opacity: 0; }
.density label { border: 1px solid var(--rule); padding: .35rem .75rem; }
.density input:checked + svg, .density label:has(input:checked) {
background: var(--brand-weak); border-color: var(--brand); font-weight: 600; /* fill + border + weight */
}
.density label:has(input:focus-visible) { outline: 3px solid var(--focus); outline-offset: 2px; }
/* Density via tokens, never transform: scale (SC 1.4.12, 2.5.8) */
.grid[data-density="compact"] { --row-pad: 4px; --cell-min-block: 32px; }
.grid[data-density="compact"] input[type="checkbox"] { inline-size: 24px; block-size: 24px; } /* target kept */
.grid td { padding-block: var(--row-pad); min-block-size: var(--cell-min-block); line-height: 1.5; }
document.querySelector('.density').addEventListener('change', (e) => {
grid.dataset.density = e.target.value;
localStorage.setItem('grid-density', e.target.value); // persisted preference
});
Keyboard & AT behaviour
Permalink to "Keyboard & AT behaviour"| Key | Action | Expected announcement | AT deviations |
|---|---|---|---|
| Tab into group | Focus checked radio | “Row density, grouping, Comfortable, radio button, checked, 2 of 3” | — |
| Left / Right arrow | Select previous / next | “Compact, radio button, checked, 1 of 3” | VoiceOver uses arrows too |
| Space | Select focused (if not already) | — | — |
| Tab | Leave the group | Next control | — |
Integration context
Permalink to "Integration context"Density interacts with designing focus indicators for dense grids — compact rows leave less room for focus rings. Target sizes in dense grids are tested in testing target size 2.5.8 in dense grids. Density tokens belong to the design system — see density tokens and target size in data grids.
Gotchas
Permalink to "Gotchas"Hidden radios that cannot be focused. display: none on the inputs removes them from keyboard access. Hide them visually with opacity or clipping, not display.
Density resetting on navigation. Users who need spacious rows should not have to re-select it on every page; persist per user.
Charts and density. If density also affects charts or KPI cards, make sure text sizes stay readable at compact settings.
Framework notes
Permalink to "Framework notes"MUI X Data Grid has density (compact, standard, comfortable) and a built-in toolbar selector; check its row heights against target size in compact. AG Grid sets density through theme parameters (rowHeight, spacing). Angular Material supports density via its theming (mat.theme density scale, down to -5); test negative densities for target size. For the control itself, React Aria RadioGroup or a native fieldset are the simplest accessible options.
// MUI X: control density from your own accessible radio group
<DataGrid density={density} … />
Worked example
Permalink to "Worked example"A logistics grid offered “Compact” as default to fit 40 rows on screen. Users with low vision and tremor reported mis-clicks on row checkboxes, and an audit measured 16-pixel targets with 2 pixels of spacing. The team made “Comfortable” the default, kept compact as an option with 24-pixel checkbox targets achieved by padding the label, and replaced the icon-only density buttons with a labelled radio group. Power users still choose compact; nobody is stuck with it.
Testing checklist
Permalink to "Testing checklist"Design system notes
Permalink to "Design system notes"Define density as a token set (row padding, minimum cell height, control sizes) with every density verified against 2.5.8 and 1.4.12, and expose a shared DensityControl that writes a user preference all grids read.
FAQ
Permalink to "FAQ"Is a segmented control a tablist or a radio group?
If it switches content panels, a tablist. If it sets a value (density, units, period), a radio group.
Do density icons need text?
The accessible name needs text; showing the text visually is better still, especially for voice control users.
Can compact density fail WCAG?
Yes, if it shrinks targets below 24 pixels without spacing, or clips text under text-spacing overrides.
Should density changes be announced?
The radio’s checked state is announced already. No extra live message is needed.
Should density follow the operating system's settings?
There is no standard density preference, but respecting browser zoom and text size is essential; never fix row heights in pixels that ignore font size.
Should density apply to every grid or per grid?
A global preference with per-grid overrides works best. Users who need spacious rows want it everywhere; a power user may want one dense grid for a specific task.
Related
Permalink to "Related"- Table and chart view tabs — when segments switch panels
- Designing focus indicators for dense grids — focus in compact rows
- Touch targets in mobile data tables — targets on small screens