Skip to main content
WCAG Patterns

WCAG 2.4.7 · Level AA · WCAG 2.0

Focus Visible

Every keyboard-operable component must have a visible focus indicator. Removing the outline with CSS and providing no replacement fails.

Principle
Operable
Guideline
Navigable
Level
AA
Added in
WCAG 2.0

What it really means

Every keyboard-operable control must have a visible focus indicator. When you Tab through a page, you should always be able to answer "where am I right now?" within a glance. This is the rule that design teams accidentally break when they outline: none away the browser default and forget to replace it.

WCAG 2.2 also introduces 2.4.13 Focus Appearance (AAA) which tightens the bar: indicators must be at least 2 CSS pixels thick and have 3:1 contrast against the unfocused state. The AA rule below only requires that an indicator exists.

Who it helps

  • Keyboard-only users, full stop. Without a visible indicator, they are typing blind.
  • Screen-magnifier users, who have a small viewport and need a clear signal to know what scrolled into view.
  • Everyone using keyboard shortcuts — anyone who lives in Tab, Cmd-K, Escape.

How to test

  1. Tab through the entire page. Every stop should have a visible indicator.
  2. Test on every background in the design system — dark sections, brand accents, images, modals. Focus rings often break on coloured surfaces.
  3. Run axe DevTools — focus-order-semantics flags focus stops on unlabelled widgets; visual focus is harder to automate so pair with manual review.

A failing pattern

/* The single worst line in the a11y world. */
*:focus { outline: none; }
 
/* Replacement indicator exists but only on hover — keyboard users lose. */
button:hover { box-shadow: 0 0 0 2px #3b82f6; }

A passing pattern

The GOV.UK pattern — used across the FasterForward family — is a two-layer indicator that survives on light and dark backgrounds:

:focus-visible {
  outline: 3px solid #ffd700;           /* yellow ring */
  outline-offset: 2px;
  box-shadow: 0 0 0 6px #1e293b;        /* dark inset, doubles contrast */
}

Yellow on light backgrounds reads as a caution cue; the dark inset gives the ring a high-contrast edge on light or dark surfaces. You can fine-tune the ring colour to your brand — just keep the contrast.

Use :focus-visible, not :focus. :focus-visible is the modern heuristic that only shows the ring for keyboard/AT users, hiding it on plain mouse clicks. This was landed in all evergreen browsers by 2022.

For custom widgets, roving tabindex usually moves a single tab stop through a group (tabs, menu, grid). Render the indicator on the active item:

<div role="tablist">
  {tabs.map((tab, i) => (
    <button
      key={tab.id}
      role="tab"
      aria-selected={i === active}
      tabIndex={i === active ? 0 : -1}
      onFocus={() => setActive(i)}
    >
      {tab.label}
    </button>
  ))}
</div>

Notes and edge cases

  • Skip links hidden until focused — still count. Make them visually appear at the top-left when they take focus.
  • Clip-path / sr-only elements that receive focus by mistake don't have a meaningful indicator. Guard them with tabindex={-1} unless you want focus there.
  • Composed controls — if a label and input are both focusable, one of them should be the focus target, not both.