Skip to content

Focus Styling

One global rule for focus indicators. No per-element overrides, no box-shadow hacks, no outline: none without a replacement.

The pattern

Define a focus color token and a single :focus-visible rule:

css
:root {
  --color-focus: oklch(0.72 0.17 162 / 0.5);
}

:focus-visible {
  outline: solid 3px var(--color-focus);
  outline-offset: 2px;
}

Every focusable element — inputs, buttons, links, custom elements — gets a visible focus ring for free. No per-element rules needed.

Why outline, not box-shadow

  • Outlines don't affect layout (no reflow on focus)
  • Outlines follow border-radius in modern browsers
  • Outlines work with outline-offset to sit outside or inside the element
  • box-shadow is consumed by overflow: hidden — outlines are not (in most cases)

Why :focus-visible, not :focus

:focus-visible only activates for keyboard navigation, not mouse clicks. Users who click a button don't need a focus ring — users who Tab to it do.

Overflow containers

When a focusable element lives inside a container with overflow: auto or overflow: hidden, an outward outline can be clipped. Fix with an inset outline on those specific elements:

css
.scrollable-list button:focus-visible {
  outline-offset: -3px;
}

This overrides the global outline-offset: 2px only where clipping is a problem. The outline renders inside the element bounds.

Removing old focus styles

When adopting this pattern, remove:

  • outline: none on inputs and buttons (the global rule replaces it)
  • Per-element :focus-visible rules that set box-shadow
  • border-color changes on focus (the outline handles the visual indicator)
  • Transition properties for box-shadow that only existed for focus effects