Animated Focus Indicators and WCAG Focus Criteria

Part of WCAG Animation Conformance in Accessible Motion Architecture.

The problem

A design system adds polish to focus: the ring fades in over 150ms and expands from the control’s edge. It looks refined in a demo. A keyboard user tabbing quickly through a form sees a sequence of half-drawn rings and, at speed, effectively nothing — each ring is still fading in when focus moves on. On another screen, a sticky footer animates up and covers the focused button in a long form.

Focus indication is a requirement with timing implications, and animation is where those implications are usually missed.

Root cause analysis: three criteria, one rule of thumb

2.4.7 Focus Visible (AA) requires that the keyboard focus indicator is visible. An indicator that has not finished appearing when focus moves is not meaningfully visible.

2.4.11 Focus Not Obscured (Minimum) (AA, WCAG 2.2) requires that the focused component is not entirely hidden by author-created content. Animated overlays, sticky headers and footers that move over content are exactly the kind of author-created content that can obscure focus.

2.4.13 Focus Appearance (AAA, WCAG 2.2) sets out how substantial the indicator must be: an area at least as large as a 2 CSS pixel perimeter around the component, and a contrast ratio of at least 3:1 between focused and unfocused states.

The practical rule that satisfies all three: the indicator itself appears instantly and does not move; anything that animates is decoration around an already-visible indicator.

Two further details matter for animated interfaces. :focus-visible applies the indicator only when the browser judges a keyboard-like interaction, which keeps rings off mouse clicks without removing them from keyboard users. And focus during transitions — a panel animating in, a route transition running — must still show an indicator; if focus moves into content that is mid-animation, the ring must be visible on it immediately, as in focus management during route transitions.

What may animate on focusThe indicator itself is not an animation surface.What may animate on focusMay animate?WhyOutline appearanceNoMust be visible immediatelyOutline colour shiftafter appearingYes, brieflyRing already visibleBackground tint behindthe controlYesDecoration, not indicationShared highlight glidingbetween itemsYesAccompanies a static ringScale of the focusedcontrolSmall onlyMust not delay or obscure the ring
The indicator itself is not an animation surface.

Step-by-step resolution

An animated focus treatment that conformsInstant ring, animated surroundings, unobscured at all times.An animated focus treatment that conforms1Apply the outline with :focus-visible and no transition.Visible on the first frame2Check the indicator's area and contrast against 2.4.11 and 2.4.13.Substantial enough to see3Animate only surrounding decoration.Polish without delay4Add scroll-margin so focused elements are not hidden behind sticky UI.Focus stays unobscured5Tab rapidly through a form and watch every stop.No half-drawn indicators6Verify the same behaviour with reduced motion enabled.
Instant ring, animated surroundings, unobscured at all times.

Production code pattern

/* The indicator: instant, substantial, high contrast. */
:where(a, button, input, select, textarea, [tabindex]):focus-visible {
  outline: 3px solid var(--color-focus);          /* >= 2px perimeter for 2.4.13 */
  outline-offset: 2px;
  transition: none;                                /* never fade or grow the ring */
}

/* Decoration: animates around an already-visible ring. */
.button {
  position: relative;
  isolation: isolate;
}
.button::before {
  content: "";
  position: absolute;
  inset: -6px;
  z-index: -1;
  border-radius: inherit;
  background: var(--color-focus-tint);
  opacity: 0;
  transition: opacity var(--duration-100) linear;
}
.button:focus-visible::before { opacity: 1; }

/* Focus must not end up behind sticky UI. */
:root {
  scroll-padding-block: calc(var(--header-h) + 8px) calc(var(--footer-h) + 8px);
}

/* Focused controls stay above animated overlays that do not need to cover them. */
.sticky-footer { z-index: 10; }
.form :focus-visible { position: relative; z-index: 11; }

@media (prefers-reduced-motion: reduce) {
  .button::before { transition: none; }            /* tint appears instantly; ring unchanged */
}
// Verify quickly: tab through and log any focused element whose ring could be obscured.
document.addEventListener('focusin', (e) => {
  const r = e.target.getBoundingClientRect();
  const hiddenTop = r.top < parseFloat(getComputedStyle(document.documentElement).scrollPaddingBlockStart);
  const hiddenBottom = r.bottom > innerHeight - parseFloat(getComputedStyle(document.documentElement).scrollPaddingBlockEnd);
  if (hiddenTop || hiddenBottom) console.warn('Focused element may be obscured:', e.target);
});

Rendering Impact: paint for the outline, composite for the decorative tint. Removing the transition from the outline also removes a per-frame paint of the ring during rapid tabbing.

:where() keeps the focus rule’s specificity at zero, so component styles can extend it without having to out-specify it — and, importantly, cannot accidentally remove it by setting outline: none at a lower specificity.

Tabbing through five fields at 150 ms intervalsWith a 150 ms fade, no ring is ever fully visible; instantly applied rings always are.Tabbing through five fields at 150 ms intervalsFaded-in ringfadingfadingfadingfading600 msInstant ringvisiblevisiblevisiblevisible600 msfadingvisible
With a 150 ms fade, no ring is ever fully visible; instantly applied rings always are.

Focus and animated overlays

2.4.11 is where animation most often causes a failure that nobody notices in review. A sticky footer that slides up when a form is edited, a chat widget that expands, a cookie banner that animates in — each can cover the control the user is focused on, and because keyboard users are the ones affected, a mouse-driven review will not reveal it.

Two defences cover most cases. scroll-padding on the scroll container keeps browser-driven focus scrolling clear of fixed UI, so tabbing to a control below a sticky footer scrolls it into the visible region rather than behind it. And giving focused controls a stacking position above non-essential overlays means a decorative panel cannot cover them. Overlays that must cover content, such as modals, should also be moving focus into themselves, which resolves the question differently.

Verification checklist

Constraints and trade-offs

  • An instant ring is less “designed” than a fading one; decoration around it can carry the polish.
  • High-contrast indicators can clash with brand colours; contrast is the requirement, not the palette.
  • scroll-padding affects all scrolling into view, including anchor links, which is usually desirable.
  • Raising focused elements in the stacking order can change how overlapping components render.
  • Windows High Contrast and forced-colours modes override indicator colours; test there too.

Frequently asked questions

Can a focus ring fade in?

No. The indicator must be visible when focus arrives; a fade means it is incomplete or invisible during rapid tabbing.

What may animate on focus, then?

Decoration around an already-visible ring: a background tint, a shared highlight that glides between items, or a small scale on the control.

What does 2.4.11 require?

That the focused component is not entirely hidden by author-created content, which includes animated overlays, sticky headers and footers.

How big should a focus indicator be?

At AAA, at least the area of a 2 CSS pixel perimeter around the component, with a contrast ratio of at least 3:1 between focused and unfocused states.