Designing Focus Indicators for Dense Data Grids
Permalink to "Designing Focus Indicators for Dense Data Grids"In a data grid, the focus indicator is how keyboard users know which cell they are on. Grids are also where focus indicators most often fail: compact rows leave no room for an outline outside the cell, neighbouring cells paint over it, the selection highlight uses the same colour, sticky headers cover the focused row, and a subtle 1px border that looked fine in a design review disappears on a laptop screen in sunlight.
This page designs a cell focus indicator that survives density, selection, stickiness and themes. It belongs to focus indicator contrast auditing, and the measurement techniques are in auditing focus indicator contrast for WCAG 2.4.11.
Spec reference
Permalink to "Spec reference"- SC 2.4.7 Focus Visible (AA) — a keyboard focus indicator is visible.
- SC 2.4.11 Focus Not Obscured (Minimum) (AA, WCAG 2.2) — the focused component is not entirely hidden by author-created content, such as sticky headers.
- SC 2.4.13 Focus Appearance (AAA, WCAG 2.2) — the indicator is at least as large as a 2 CSS pixel thick perimeter of the component and has 3:1 contrast between focused and unfocused states. Not required at AA, but a practical design target for grids.
- SC 1.4.11 Non-text Contrast (AA) — visual information required to identify states, including focus, has 3:1 against adjacent colours.
In a grid, “adjacent colours” include the cell background, the selected-cell background, grid lines and neighbouring cells — not only the page background.
When to use an inset ring — and when outside outlines work
Permalink to "When to use an inset ring — and when outside outlines work"Use an inset indicator (outline-offset: -2px or an inset box-shadow with a solid fallback) for cells in grids with no gap between cells. Outlines drawn outside the cell are painted under neighbouring cells in many layouts, especially with sticky columns, and end up visible on only two sides.
Outside outlines work for controls with space around them — the grid’s toolbar buttons, the column header menu buttons — and for the grid container itself when it receives focus.
The misapplication to name is using the brand’s selection colour for focus as well. When a user moves through a selected range, they cannot tell which cell is focused — the whole range looks focused.
Annotated code example
Permalink to "Annotated code example":root {
--cell-bg: #ffffff;
--grid-line: #dde8e1;
--focus: #155c38; /* 8.0:1 on white — well above 3:1 */
--selected-bg: #d6f9e8; /* tint: the ring must still reach 3:1 on this */
--selected-edge: #219150;
}
[data-theme="dark"] {
--cell-bg: #141d18;
--grid-line: #2c3a32;
--focus: #6ddfa0;
--selected-bg: #173625;
--selected-edge: #3ec97a;
}
/* SC 2.4.7 + 2.4.13 target: 2px perimeter, inside the cell so neighbours cannot clip it */
[role="gridcell"]:focus-visible,
[role="columnheader"]:focus-visible {
outline: 2px solid var(--focus);
outline-offset: -2px;
}
/* Selection: tint + edge bar, never the focus colour (SC 1.4.1, 1.4.11) */
[role="gridcell"][aria-selected="true"] {
background: var(--selected-bg);
box-shadow: inset 3px 0 0 var(--selected-edge);
}
/* SC 2.4.11: keep focused cells clear of sticky header and frozen column */
.grid-scroll {
scroll-padding-block-start: var(--sticky-header-h, 2.5rem);
scroll-padding-inline-start: var(--frozen-col-w, 10rem);
}
/* Forced colours: box-shadow disappears; outline survives with a system colour */
@media (forced-colors: active) {
[role="gridcell"]:focus-visible { outline-color: Highlight; }
[role="gridcell"][aria-selected="true"] { outline: 1px solid SelectedItem; outline-offset: -4px; }
}
The ring uses outline rather than box-shadow so it survives forced colours mode, where shadows are removed. Selection uses box-shadow for the edge bar because in forced colours the media query replaces it with an outline in SelectedItem.
Keyboard & AT behaviour
Permalink to "Keyboard & AT behaviour"| Situation | Expected visual | Failure indicator |
|---|---|---|
| Arrow to a cell | 2px inset ring, all four sides visible | Ring clipped on right and bottom |
| Arrow within a selected range | Ring distinct from the tint | Cannot tell focused from selected |
| Arrow to a row under the sticky header | Row scrolls into full view | Row hidden under header (SC 2.4.11) |
| Dark theme | Ring in the lighter focus colour | Dark ring on dark cells |
| Forced colours | Ring in Highlight | No ring (box-shadow removed) |
| 200% zoom | Ring scales; still 2 CSS px | Hairline ring at 1 device pixel |
Integration context
Permalink to "Integration context"Automating the check on every build — screenshotting focused cells in both themes and measuring the ring — is covered in automating focus indicator screenshots with Playwright. Sticky headers and frozen columns, the main cause of obscured focus in grids, are covered in sticky table headers and first columns.
Selection design — tint plus edge bar — follows the selection model in aria-selected on cells versus rows.
Gotchas
Permalink to "Gotchas"Contrast against the tint. A ring that passes against white can fail against the selected tint. Measure against every background the focused cell can have.
Row focus versus cell focus. Grids with row-level focus need the ring around the whole row, which is easiest with an outline on the row element and outline-offset: -2px.
Hover styles. Hover highlights that look like focus confuse keyboard users watching the screen. Make hover lighter and clearly different from the focus ring.
Design system notes
Permalink to "Design system notes"Define grid focus and selection as a small token set — --focus, --selected-bg, --selected-edge — per theme, with measured ratios against the cell background, the selected background and the grid line recorded in the token docs. Grid components apply them through :focus-visible and [aria-selected] selectors only.
Testing checklist
Permalink to "Testing checklist"FAQ
Permalink to "FAQ"Why do focus outlines disappear in data grids?
Because cells sit edge to edge, and an outline drawn outside a cell is painted under its neighbours. Draw the indicator inside the cell with a negative outline offset.
Can the focus indicator use the same colour as selection?
It should not. Users moving through a selected range need to see which cell is focused. Use a ring for focus and a tint with an edge bar for selection.
What does SC 2.4.11 mean for grids with sticky headers?
A focused cell must not be completely hidden by the sticky header or a frozen column. Add scroll padding so the browser scrolls focused cells into the visible area.
Is SC 2.4.13 Focus Appearance required?
It is Level AAA, so not required for AA conformance, but its 2px perimeter and 3:1 change targets are a good, measurable design standard for grid focus indicators.
Related
Permalink to "Related"- Auditing focus indicator contrast — how to measure
- Automating focus indicator screenshots — checking every build
- Sticky headers and first columns — the obscuring problem