Focus Appearance (2.4.13) for Grid Cells
Permalink to "Focus Appearance (2.4.13) for Grid Cells"SC 2.4.13 Focus Appearance (AAA in WCAG 2.2) sets a measurable bar for focus indicators: the indicator must be at least as large as a 2 CSS pixel thick perimeter around the component and have at least 3:1 contrast between the same pixels in focused and unfocused states. It is AAA, but it is the clearest definition of a good focus indicator, and grid cells — small, dense, often coloured by selection — are where indicators most often fall short.
This guide belongs to focus indicator contrast auditing.
Spec reference
Permalink to "Spec reference"- SC 2.4.13 Focus Appearance (AAA) — area at least that of a 2 px perimeter; 3:1 contrast change between focused and unfocused states.
- SC 2.4.7 Focus Visible (AA) — some visible indicator.
- SC 1.4.11 Non-text Contrast (AA) — indicator contrast against adjacent colours.
- Not obscured — 2.4.13 also expects the indicator not to be hidden by author content.
When to use — and when not to
Permalink to "When to use — and when not to"Aim for 2.4.13 in data grids even if you only target AA; it gives designers and testers a precise rule. Apply it to every focusable cell, header and control in grids.
Do not rely on a background colour change alone for focus; it often fails the 3:1 change and collides with selection. The misapplication: a 1 px light grey outline on a white cell, technically “visible” and practically invisible.
Annotated code example
Permalink to "Annotated code example".grid [role="gridcell"]:focus,
.grid [role="columnheader"]:focus {
outline: 2px solid var(--focus-color, #111714); /* 2 px perimeter */
outline-offset: -2px; /* inside, avoids clipping by neighbours */
box-shadow: inset 0 0 0 4px var(--focus-halo, #ffffff); /* second tone for coloured cells */
position: relative; z-index: 1;
}
.grid [aria-selected="true"] { background: var(--selected-bg, #d6f9e8); } /* selection: background */
@media (forced-colors: active) {
.grid [role="gridcell"]:focus { outline-color: Highlight; box-shadow: none; }
}
// Rough audit: compare pixels of a cell focused versus unfocused (Playwright screenshots)
const before = await cell.screenshot(); await cell.focus(); const after = await cell.screenshot();
const changed = diffPixels(before, after, { minContrast: 3 }); // count pixels whose contrast change ≥ 3:1
expect(changed).toBeGreaterThanOrEqual(perimeterArea(box, 2));
Keyboard & AT behaviour
Permalink to "Keyboard & AT behaviour"| State | Visual | Requirement |
|---|---|---|
| Focused cell | 2 px dark outline + light inner halo | Area and 3:1 change |
| Selected cell | Tinted background | Distinct from focus |
| Focused and selected | Outline on tinted background | Outline still 3:1 against tint |
| Hover | Subtle background | Must not look like focus |
| Forced colours | Highlight outline | System colour |
Step-by-step implementation
Permalink to "Step-by-step implementation"- Compute the requirement. For a cell of width w and height h, a 2 px perimeter has area about 4(w + h) pixels; that is the minimum indicator area.
- Choose a technique. An inset 2 px outline plus an inner halo works in dense grids without clipping.
- Check contrast change. Compare the indicator pixels focused versus unfocused; they must differ by 3:1. Against coloured selection backgrounds, the second tone ensures this.
- Separate states. Selection uses background; focus uses outline; hover uses a subtle background that cannot be confused with either.
- Verify themes. Check dense mode, dark mode and forced colours; take screenshots of each state in CI.
Integration context
Permalink to "Integration context"Contrast auditing is covered in auditing focus indicator contrast and dense grid design in designing focus indicators for dense grids. Screenshot automation is in automating focus indicator screenshots with Playwright. Obscured focus is in focus not obscured by sticky headers, and tokens in design system tokens and component contracts.
Gotchas
Permalink to "Gotchas"Outline clipping. Neighbouring cells with backgrounds paint over outlines outside the cell; inset outlines or raised z-index avoid it.
:focus-visible. Grids that use roving tabindex often want focus visible on mouse clicks too, since users switch between mouse and keyboard; decide deliberately.
Rounded corners. The perimeter is measured on the component’s shape; rounded outlines are fine if thick enough.
Edge cases
Permalink to "Edge cases"- Very small cells. Icon cells of 24 px still need a 2 px perimeter indicator; inset outlines may cover content, so use an outside outline with z-index.
- Merged cells. Larger cells need larger indicators by area; a 2 px outline scales automatically.
- Images in cells. Thumbnails can reduce contrast; the inner halo helps.
Worked example
Permalink to "Worked example"A spreadsheet-style grid used a 1 px blue outline for focus and a light blue background for selection. On selected cells, the focus outline was nearly invisible, and low-vision testers lost their place. The team switched to a 2 px dark inset outline with a white inner halo, kept selection as a background tint and verified a 3:1 change with screenshot diffs. Testers could track focus across selected ranges.
Common mistakes
Permalink to "Common mistakes"- 1 px outlines.
- Background-only focus.
- Focus that looks like selection.
- Outlines clipped by neighbours.
Testing checklist
Permalink to "Testing checklist"Testing it with a screen reader
Permalink to "Testing it with a screen reader"Focus appearance is visual, but sighted screen reader users depend on it to follow the virtual cursor. With NVDA running in focus mode, arrow through a grid and confirm the outline moves with each announcement; NVDA’s own focus highlighter can be turned on to compare. Mismatches between NVDA’s highlight and your outline reveal focus management bugs as well as appearance issues.
Measuring without special tools
Permalink to "Measuring without special tools"You can audit 2.4.13 with a browser and a colour picker. Zoom a screenshot of a focused cell, count the outline thickness in pixels and compare its length to the cell perimeter. Sample the outline colour focused and the same pixels unfocused, and compute the contrast ratio with any contrast checker. Repeat on a selected cell and in dark mode. Record the numbers in the design system documentation, so designers can see why the indicator looks the way it does.
Key points
Permalink to "Key points"- Make the indicator at least a 2 px perimeter, inset in dense grids.
- Guarantee a 3:1 change with a second tone on coloured cells.
- Keep focus, selection and hover visually distinct.
- Verify in dense, dark and forced colours modes.
Design system notes
Permalink to "Design system notes"Define focus tokens (width, colours, halo) for grid cells, document the 2.4.13 calculation and test every grid density.
FAQ
Permalink to "FAQ"Is 2.4.13 required?
It is AAA; many teams adopt it as a design rule.
Does an outline outside the cell count?
Yes, if not clipped.
Can I use box-shadow?
Yes, if area and contrast requirements are met; check forced colours.
How do I measure contrast change?
Compare the same pixels focused and unfocused.
What about selected cells?
Use a second tone so the indicator contrasts with selection colours.
Do headers need the same indicator?
Yes, every focusable element in the grid.
Related
Permalink to "Related"- Designing focus indicators for dense grids — design
- Focus not obscured by sticky headers — obscured focus
- Focus indicators for chart data points — charts