Home / Snippets / UI Components /

Focus ring

Accessible keyboard focus indicators using :focus-visible, outline-offset, and box-shadow — no JavaScript needed.

Default outline Link
Offset outline Link
Rounded (pill) Link
Double ring Link
Animated ring Link

Tab through the elements above to see each focus style.

Widely Supported
uino-js

Quick implementation

/* 1. Default outline */
.btn:focus-visible {
  outline: 2px solid oklch(0.72 0.19 265);
  outline-offset: 2px;
}

/* 2. Spaced-out offset */
.btn--spaced:focus-visible {
  outline: 2px solid oklch(0.72 0.19 265);
  outline-offset: 5px;
}

/* 3. Rounded / pill button */
.btn--pill:focus-visible {
  outline: 2px solid oklch(0.72 0.19 265);
  outline-offset: 4px;
  border-radius: 9999px;
}

/* 4. Double ring via box-shadow */
.btn--double:focus-visible {
  outline: 2px solid oklch(0.72 0.19 265);
  outline-offset: 2px;
  box-shadow: 0 0 0 4px oklch(0.72 0.19 265 / 0.25);
}

/* 5. Animated pulse ring */
.btn--animated:focus-visible {
  outline: none;
  box-shadow:
    0 0 0 2px var(--bg),
    0 0 0 4px oklch(0.72 0.19 265);
  animation: focus-pulse 1.5s ease-in-out infinite;
}

@keyframes focus-pulse {
  0%, 100% {
    box-shadow:
      0 0 0 2px var(--bg),
      0 0 0 4px oklch(0.72 0.19 265);
  }
  50% {
    box-shadow:
      0 0 0 2px var(--bg),
      0 0 0 6px oklch(0.72 0.19 265 / 0.55);
  }
}

@media (prefers-reduced-motion: reduce) {
  .btn--animated:focus-visible {
    animation: none;
  }
}

Prompt this to your LLM

Includes role, constraints, two framework variants, and edge cases to handle.

You are a senior frontend engineer implementing accessible keyboard
focus indicators for a UI component library.

Goal: Create custom focus ring styles for buttons and links using
:focus-visible so mouse users never see the outline, but keyboard
users always get a clear, high-contrast indicator.

Technical constraints:
- Always use :focus-visible (not :focus) to target keyboard navigation
  only. Mouse clicks do not trigger :focus-visible.
- Use outline and outline-offset as the primary technique — outlines
  follow border-radius automatically and don't affect layout.
- For pill/fully-rounded elements, explicitly repeat border-radius on
  the outline so it tracks the shape (some browsers need this hint).
- For double-ring effects, layer outline with a translucent box-shadow.
- Use oklch() for all colors — no hex or rgba values.
- Use CSS custom properties (var(--bg), var(--accent), etc.) for
  theming tokens.
- Include @media (prefers-reduced-motion: reduce) that disables any
  focus animation while keeping the static ring visible.

Framework variant (pick one):
A) Global CSS reset — apply :focus-visible styles to all interactive
   elements (button, a, input, select, textarea) via a single rule,
   then override per-component as needed.
B) React component — a FocusRing wrapper that renders its children
   inside a span with the focus class applied; accept a variant prop
   ("default" | "offset" | "double" | "animated").

Edge cases to handle:
- Removing outline: 0 on :focus without restoring it on :focus-visible
  is a WCAG 2.4.7 violation. Never suppress focus entirely.
- Input elements on dark backgrounds may need a contrasting gap between
  the element and the ring — use box-shadow: 0 0 0 2px var(--bg) as
  an inner spacer before the visible ring.
- Safari (before v15.4) did not support :focus-visible; use a
  progressive enhancement approach and test accordingly.
- Animated rings must respect prefers-reduced-motion — the animation
  should stop, but the ring itself must remain visible.

Return CSS only (or a React component if variant B is chosen).

Why :focus-visible beats :focus

For most of the web's history, developers faced a frustrating trade-off: keep the browser's default focus ring (which fires on mouse clicks and looks jarring to mouse users) or suppress it entirely with outline: none (which destroys keyboard navigation). The introduction of :focus-visible — now at 96 %+ global browser support — resolves this completely.

The browser applies :focus-visible using a heuristic: when you navigate with a keyboard, Tab key, or other non-pointer device, the pseudo-class activates. When you click with a mouse or tap on a touchscreen, it does not. The result is that keyboard users always see the indicator, and mouse users never do — without any JavaScript.

WCAG 2.4.7 (Level AA) requires that any keyboard-operable interface component has a visible focus indicator. Relying on :focus-visible — combined with a well-contrasting outline color — is the cleanest compliant implementation.

outline vs box-shadow for rounded elements

CSS outline has been the recommended approach since the spec clarified that outlines must follow the element's border-radius. All modern browsers now render a pill-shaped outline on a fully-rounded button. outline-offset creates a gap between the element's border and the ring — positive values push the ring outward, giving breathing room without affecting layout.

The limitation of outline is that it only supports a single ring. For a double-ring effect — a dark background gap followed by a colored ring — you layer box-shadow. Use an inner shadow of 0 0 0 2px var(--bg) to fake a gap, then a second shadow of 0 0 0 4px oklch(0.72 0.19 265) for the visible ring. box-shadow is clipped to the element's bounding box so it follows border-radius naturally.

One important distinction: outline does not trigger layout changes — it draws outside the element's box model and never shifts surrounding elements. box-shadow behaves the same way. Avoid using border alone for focus rings because changing border-width on focus causes layout shift.

WCAG 2.4.7 and focus visibility requirements

WCAG Success Criterion 2.4.7 (Focus Visible) at Level AA states: "Any keyboard operable user interface has a mode of operation where the keyboard focus indicator is visible." This means suppressing outline on :focus without replacing it is a direct accessibility failure — one that automated audit tools like axe and Lighthouse will flag.

WCAG 2.2 introduced the enhanced 2.4.11 (Focus Appearance, Level AA) with tighter requirements: the focus indicator must have an area of at least the perimeter of the component times 2 CSS pixels, and a contrast ratio of at least 3:1 between focused and unfocused states. A 2 px outline in oklch(0.72 0.19 265) on a dark background comfortably meets this threshold.

For animated focus rings, prefers-reduced-motion: reduce must stop the animation — but the ring itself must stay visible. Setting animation: none inside the media query while leaving the box-shadow values intact is the correct pattern. Never hide the ring in the reduced-motion query.