Design and implement keyboard focus indicators that meet WCAG 2.2 requirements — visible, high-contrast, and consistently applied across all interactive elements without affecting visual design.
Design and implement keyboard focus indicators that meet WCAG 2.2 requirements — visible, high-contrast, and consistently applied across all interactive elements without affecting visual design.
Keyboard focus indicators are the visual equivalent of the mouse cursor for keyboard users. Without visible focus, users navigating with Tab or arrow keys have no way to know which element is currently active — they are effectively operating blind. Approximately 7% of users with disabilities rely primarily on keyboard navigation, and focus indicators also benefit power users, people with tremors who cannot use a mouse precisely, and users with motor impairments using switch access or voice control.
The most common accessibility mistake in web development is adding outline: none or outline: 0 to interactive elements for visual polish. This directly violates WCAG 2.4.7 (Focus Visible, Level AA) and is one of the most impactful accessibility failures because it affects every keyboard-dependent user interaction on the page. Never remove focus styling without immediately replacing it with an equally or more visible alternative.
WCAG 2.1 requires focus to be visible (2.4.7, AA) but does not specify how visible. WCAG 2.2 added two new criteria: 2.4.11 Focus Appearance Minimum (AA) requires at minimum a 2px perimeter outline with 3:1 contrast against adjacent colors, and 2.4.12 Focus Appearance Enhanced (AAA) sets higher thresholds.
WCAG 2.2 success criterion 2.4.11 (Focus Appearance, AA) specifies that the focus indicator must:
The practical implementation: a 3px offset outline in a high-contrast color satisfies all of these requirements for most components:
1/* System that passes 2.4.11 Focus Appearance */
2:root {
3 --focus-ring-color: #1D4ED8; /* 8.6:1 on white */
4 --focus-ring-color-dark: #93C5FD; /* 4.7:1 on dark bg */
5 --focus-ring-width: 3px;
6 --focus-ring-offset: 2px;
7}
8
9:focus-visible {
10 outline: var(--focus-ring-width) solid var(--focus-ring-color);
11 outline-offset: var(--focus-ring-offset);
12 /* No border-radius override — let it match the element */
13}
14
15@media (prefers-color-scheme: dark) {
16 :focus-visible {
17 outline-color: var(--focus-ring-color-dark);
18 }
19}The :focus pseudo-class triggers for both keyboard navigation and mouse clicks. Showing a focus ring when a user clicks a button with a mouse feels visually jarring and is why designers remove focus styles. The solution is :focus-visible — the browser only applies these styles when the focus was obtained via keyboard, not mouse.
1/* Remove default outline */
2* {
3 outline: none;
4}
5
6/* Re-add outline ONLY for keyboard navigation */
7*:focus-visible {
8 outline: 3px solid var(--focus-ring-color);
9 outline-offset: 2px;
10 border-radius: inherit; /* Match component's border-radius */
11}
12
13/* Component-specific focus styles */
14.button:focus-visible {
15 /* For buttons with round corners, the outline follows the shape */
16 border-radius: calc(var(--button-radius) + 2px);
17}
18
19.link:focus-visible {
20 /* Inline links: underline + box */
21 outline: 2px solid var(--focus-ring-color);
22 outline-offset: 1px;
23 border-radius: 2px;
24}
25
26.input:focus-visible {
27 /* Inputs: show inside the border, not offset */
28 outline: 3px solid var(--focus-ring-color);
29 outline-offset: 0;
30}:focus-visible has 97%+ browser support in 2026. Do not use the polyfill anymore — it adds unnecessary complexity.
Custom components built with div and span instead of native HTML elements need explicit focus management:
1// Custom Chip component with proper focus indicator
2interface ChipProps {
3 label: string;
4 onRemove: () => void;
5 selected?: boolean;
6}
7
8export function Chip({ label, onRemove, selected }: ChipProps) {
9 return (
10 <span
11 role="option" /* Correct ARIA role */
12 aria-selected={selected}
13 tabIndex={0} /* Makes it keyboard focusable */
14 onKeyDown={(e) => {
15 if (e.key === 'Delete' || e.key === 'Backspace') onRemove();
16 }}
17 className="chip"
18 >
19 {label}
20 <button
21 onClick={onRemove}
22 aria-label={`Remove ${label}`}
23 className="chip-remove"
24 >
25 ×
26 </button>
27 </span>
28 );
29}1.chip:focus-visible {
2 outline: 3px solid var(--focus-ring-color);
3 outline-offset: 2px;
4 border-radius: 999px; /* Match chip's pill shape */
5}
6
7.chip-remove:focus-visible {
8 /* Remove button inside chip needs its own focus indicator */
9 outline: 2px solid var(--focus-ring-color);
10 outline-offset: 1px;
11 border-radius: 50%;
12}Native HTML elements (button, a, input, select, textarea) get keyboard focusability for free. Custom interactive elements need tabIndex={0} and explicit keyboard event handlers.
Define focus indicator tokens in your design system to ensure consistency across all components and enable easy updates:
1/* Design system focus tokens */
2:root {
3 /* Color: high contrast against both white and dark surfaces */
4 --ds-focus-color: #1D4ED8; /* blue-700: 8.6:1 on white */
5 --ds-focus-color-dark: #93C5FD; /* blue-300: 4.7:1 on gray-950 */
6
7 /* Size: 3px passes WCAG 2.4.11 4px² minimum area requirement */
8 --ds-focus-width: 3px;
9
10 /* Offset: creates visible separation from the component boundary */
11 --ds-focus-offset: 2px;
12
13 /* On dark backgrounds — white ring with dark inner ring */
14 --ds-focus-color-on-dark: #ffffff;
15 --ds-focus-shadow-on-dark: 0 0 0 2px #000000; /* Inner ring */
16}
17
18/* Global application */
19:focus-visible {
20 outline: var(--ds-focus-width) solid var(--ds-focus-color);
21 outline-offset: var(--ds-focus-offset);
22 border-radius: 2px; /* Default, overridden per component */
23}
24
25/* Dark mode auto-switch */
26[data-color-scheme='dark'] :focus-visible,
27@media (prefers-color-scheme: dark) {
28 :focus-visible {
29 outline-color: var(--ds-focus-color-dark);
30 }
31}
32
33/* High-contrast mode: use system Highlight color */
34@media (forced-colors: active) {
35 :focus-visible {
36 outline-color: Highlight;
37 }
38}Audit your current application for outline: none or outline: 0 CSS rules. For each one found, check whether a :focus-visible replacement exists. Replace any missing focus styles with the token-based system from this lesson.
Your progress is saved in this browser, on this device only.