Home / Snippets / UI /

outline-offset focus ring

A focus ring with a visible gap between the element and the ring, using outline combined with outline-offset for clean keyboard navigation styling that meets WCAG 2.2.

Tab through the elements below to see the focus rings

Link
Widely Supported
uino-js

Quick implementation

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 4px; /* gap between element and ring */
  border-radius: 2px;  /* optional: match element's radius */
}

/* Remove default outline only when providing a custom one */
:focus:not(:focus-visible) {
  outline: none;
}

Prompt this to your LLM

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

You are a senior frontend engineer building accessible keyboard navigation styles.

Goal: A focus ring using outline + outline-offset that creates a visible gap
between the element and its focus indicator — no JavaScript, no box-shadow hacks.

Technical constraints:
- Use outline (not box-shadow) so the ring appears outside the border box
  without affecting layout.
- Use outline-offset with a positive value (2px–6px) for the gap.
- Target :focus-visible (not :focus) to avoid showing the ring on mouse clicks.
- Use oklch() for all color values — no hex or rgba.
- The outline color must have 3:1 contrast against the adjacent background
  per WCAG 2.2 SC 2.4.11.

Framework variant (pick one):
A) Vanilla CSS — a global :focus-visible rule plus per-component overrides.
B) React component — a FocusRing wrapper that injects the outline styles
   via CSS-in-JS (styled-components or CSS Modules).

Edge cases to handle:
- On dark backgrounds, use a light outline color; on light backgrounds a
  dark one — derive from oklch() by adjusting lightness.
- If the element already has a border-radius, add the same border-radius
  to the outline so it follows the element's shape.
- Windows High Contrast Mode ignores custom outline colors — test with the
  forced-colors media query to ensure the ring is still visible.

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

Why outline and outline-offset are the canonical focus pattern

outline-offset has been widely supported for years but was underused because developers often removed focus styles entirely. In 2026, after WCAG 2.2 introduced a Success Criterion specifically for focus appearance (minimum area, contrast), outline combined with outline-offset is the canonical pattern — it works across all element types without affecting layout and satisfies the new requirements without custom calculations.

The gap created by outline-offset serves a visual purpose beyond aesthetics: it separates the focus indicator from the element's own background colour, ensuring the ring reads clearly against both the element and the surrounding page — a key requirement of WCAG 2.2 SC 2.4.11 Non-text Contrast.

How outline, outline-offset, and :focus-visible work together

outline draws outside the border box without affecting layout (unlike box-shadow, which can be clipped by overflow: hidden on a parent). outline-offset pushes the outline away from the element's edge — positive values create a visible gap, negative values inset the ring inside the element. A negative offset can be useful for table cells or full-bleed elements where an external ring would overlap surrounding content.

:focus-visible (not :focus) targets only keyboard navigation, not mouse clicks, so mouse users do not see the ring on every button tap. The browser's heuristic is reliable: it shows the ring when the user is navigating by keyboard, switch access, or other sequential navigation methods. The companion rule :focus:not(:focus-visible) { outline: none } suppresses the browser default for mouse users while preserving it for keyboard users in older browsers that support :focus but not yet :focus-visible.

Adding border-radius to the :focus-visible rule matches the outline's corners to the element's shape. Browsers follow the element's border-radius for outline automatically in modern engines, but explicitly declaring it ensures consistent rendering and allows you to specify a tighter or looser radius than the element itself.

WCAG 2.2, contrast, and Windows High Contrast Mode

Never set outline: none without providing a replacement visible indicator. Use outline-offset: 4px as a starting point — WCAG 2.2 SC 2.4.11 requires the focus indicator to have a minimum area equal to the perimeter of the unfocused component times 2 CSS pixels. A 2 px outline at 4 px offset satisfies this for most button sizes.

outline does not trigger layout reflow, making it the highest-performance focus indicator option. box-shadow is repainted on every frame it changes; outline is not. For animated focus rings (transitioning outline-offset from 0 to 4 px on focus), use a short transition: outline-offset 0.1s ease — the transition is smooth and composited.

Test with Windows High Contrast Mode (forced-colors media query) — in that mode, the browser overrides custom outline colours with system colours, which is the correct behaviour. Avoid using outline-color: transparent as a visibility toggle, as this makes the ring invisible in High Contrast Mode. Use outline-width: 0 or remove the property entirely when you want no ring.