← Back to gallery
CSS

Custom Focus Indicator Variants

Each :focus-visible treatment pairs a non-color signal with a color signal so focus stays legible across contrast modes. Three pairings: a Solid Ring (outline + offset on a form CTA), Underline + Color (text-decoration on an inline link), and Soft Glow (layered box-shadow on a pill subscribe button).

focus-visiblefocus-ringoutline-offsetbox-shadowforced-colorskeyboard-a11yprefers-reduced-motion

Form · :focus-visible · non-color signal

Custom Focus Indicator Variants

Three :focus-visible treatments that each pair a non-color signal with a color signal so focus stays legible across contrast modes — Solid Ring (outline + offset on a form CTA), Underline + Color (text-decoration on an inline link), and Soft Glow (layered box-shadow on a pill subscribe button). Stage cards cycle the focus on/off every 1.6s so the entry/exit motion plays without input; inspector preview is a real <button> you can Tab to.

Form CTA · outline + offset

Solid Ring

A solid outline sits offset outside the control, anchoring focus with maximum contrast while staying outside the rendering box. Shown on a form CTA so the ring reads against a real input row above. Strongest option for dense UI; works across themes and forced-colors mode because outline carries non-color shape and color together.

  • outline
  • offset
  • high contrast

Inline link · text-decoration on the label

Underline + Color

Focus-visible draws an underline plus a color shift on the label. Shown inside a real sentence so the focus reads as a link rather than a button — ideal for prose-embedded actions and breadcrumbs where a full outline would feel heavy. The underline survives forced-colors mode where chroma alone would not.

  • underline
  • color shift
  • inline link

Pill CTA · layered box-shadow halo

Soft Glow

A soft box-shadow ring follows the border-radius and layers above the background, preserving focus visibility on heavily rounded buttons and pill controls. Shown as a marketing-style subscribe CTA so the halo reads against a dim panel. The shadow is doubled — a thin dark separator beneath an outer accent halo — so it works on both light and dark surfaces.

  • box-shadow
  • pill
  • rounded

Focus indicator inspector

Solid Ring

press tab
  • outline
  • offset
  • high contrast

A solid outline sits offset outside the control, anchoring focus with maximum contrast while staying outside the rendering box. Shown on a form CTA so the ring reads against a real input row above. Strongest option for dense UI; works across themes and forced-colors mode because outline carries non-color shape and color together.

Helped you ship something? 🐟 Send my cat a churu

/* A solid outline offset outside the control. Carries non-color shape + color, survives forced-colors mode. */
.focus-target {
  padding: 11px 22px;
  border-radius: 10px;
  background: linear-gradient(180deg, #1e293b, #0b1220);
  color: rgba(241, 245, 249, 0.95);
  border: 1px solid rgba(125, 211, 252, 0.32);
  font: inherit;
  font-weight: 600;
  letter-spacing: 0.01em;
  cursor: pointer;
  outline: 0 solid transparent;
  transform: scale(0.985);
  transition:
    transform 0.18s cubic-bezier(0.45, 1.4, 0.5, 1),
    outline-color 0.18s ease,
    outline-width 0.18s ease;
}

.focus-target:focus-visible {
  outline: 2px solid #7dd3fc;
  outline-offset: 3px;
  transform: scale(1);
}

How to make this

A custom focus indicator uses :focus-visible on a real control, pairs color with a shape signal such as outline or underline, and keeps forced-colors behavior intact.

html
1<form class="focus-indicator-demo">  <label>    <span>Email</span>    <input type="email" value="[email protected]">  </label>6  <button class="focus-indicator-demo__button" type="submit">    Save changes  </button></form> <style>.focus-indicator-demo {  display: grid;  gap: .8rem;  width: min(100%, 20rem);  color: #e2e8f0;  font: 600 .92rem/1.25 ui-sans-serif, system-ui;}.focus-indicator-demo label {  display: grid;  gap: .35rem;  color: rgba(226, 232, 240, .74);}.focus-indicator-demo input,.focus-indicator-demo__button {  border-radius: 10px;  font: inherit;}.focus-indicator-demo input {  border: 1px solid rgba(148, 163, 184, .35);  padding: .65rem .75rem;  color: #f8fafc;  background: rgba(15, 23, 42, .72);}.focus-indicator-demo__button {  border: 1px solid rgba(125, 211, 252, .3);  padding: .7rem 1rem;  color: #f8fafc;  background: linear-gradient(180deg, #1e293b, #0f172a);40  outline: 0 solid transparent;  outline-offset: 0;  transform: scale(.985);43  transition: outline-color .18s ease, outline-width .18s ease, transform .18s ease;}.focus-indicator-demo__button:focus-visible {46  outline: 3px solid #7dd3fc;  outline-offset: 4px;  transform: scale(1);}50@media (prefers-reduced-motion: reduce) {  .focus-indicator-demo__button { transition: none; transform: none; }}53@media (forced-colors: active) {  .focus-indicator-demo__button:focus-visible { outline-color: Highlight; }}</style>

Annotated snippet

  1. Line 1Use the indicator inside real form markup. The pattern should improve native focus, not replace the browser focus model with a custom div.
    PitfallWhy use :focus-visible instead of :focus?

    :focus-visible targets keyboard-like focus heuristics, so mouse clicks do not always show a heavy ring while keyboard navigation does. For custom widgets, keep testing because focus-visible behavior depends on actual focus movement.

  2. Line 6The target remains a real button, so keyboard activation, submit behavior, and screen reader semantics come from the platform.
    PitfallShould I remove the browser outline before adding a custom focus style?

    Only if you replace it immediately with an equally visible :focus-visible style. Never ship outline: none without a tested alternative, because it can make keyboard navigation invisible.

  3. Line 40The rest state reserves an outline style but keeps it transparent and zero-width. The focus state can transition in without changing the button layout box.
    PitfallShould I remove the browser outline before adding a custom focus style?

    Only if you replace it immediately with an equally visible :focus-visible style. Never ship outline: none without a tested alternative, because it can make keyboard navigation invisible.

  4. Line 43Whitelist only focus-related properties. Avoid transition: all because a future padding, width, or color change could start animating accidentally.

    Both buttons get the same colour ring at the same focus moment. A 1px outline reads as a thin tint that loses against the button border at a glance; a 3px outline reaches the contrast budget keyboard users need.

    PitfallHow thick should a custom focus ring be?

    Use a visible non-color shape, usually at least 2px with enough contrast and offset. Dense UI often needs 2-3px plus outline-offset so the ring does not merge with nearby borders.

  5. Line 46outline plus outline-offset is the most robust focus shape. The offset keeps the ring readable when the control is next to borders or dense form rows.

    A ring with no offset can blend into the control edge. Offset gives the focus shape breathing room.

    PitfallHow thick should a custom focus ring be?

    Use a visible non-color shape, usually at least 2px with enough contrast and offset. Dense UI often needs 2-3px plus outline-offset so the ring does not merge with nearby borders.

  6. Line 50Reduced motion removes the small scale entry but leaves the focus ring itself untouched. Visibility must not depend on motion.
    PitfallDo focus indicators need prefers-reduced-motion?

    Only the entry motion needs reduction. The focus indicator itself must remain visible, so disable scale or glow transitions while keeping outline, underline, or shadow state intact.

  7. Line 53Forced-colors mode can override brand colors. Mapping the ring to Highlight keeps the indicator visible in high-contrast environments.
    PitfallIs box-shadow enough for focus indicators?

    Box-shadow is useful for rounded pills, but it can disappear in forced-colors mode. Pair it with an outline fallback or use outline as the primary focus shape when reliability matters.

Notes

Overview

Custom focus indicators pair a non-color signal (outline, underline, glow ring) with a color signal so focus stays legible in any contrast mode, including forced-colors and high-contrast OS modes. Three pairings ship: solid ring on a form CTA, underline + color on an inline link, soft glow on a pill subscribe button.

When to use it

Reach for custom focus indicators when your default browser focus ring clashes visually with the design system. Skip custom indicators on critical UI without keeping the original contrast envelope — tinted glows that disappear on dark themes break keyboard nav. Always pair a custom indicator with :focus-visible so mouse users do not see it on every click.

How it works

The pattern leans on :focus-visible — the browser’s own heuristic for “was this focus reached via keyboard or assistive tech” — so the indicator only renders when it actually helps. For each variant: the ring uses outline (not box-shadow) so it survives forced-colors mode where box-shadow gets stripped. The underline variant draws via a ::after with transform-origin: left + a transition; the glow uses a layered outline + box-shadow stack so the box-shadow handles the soft halo while the outline guarantees a forced-colors fallback.

Production gotchas

Stripping the default focus ring with outline: none without replacing it is the single most common accessibility regression on modern sites. Always pair the strip with a :focus-visible custom indicator. In forced-colors mode every author color is replaced with system colors, so any indicator that depended on a specific hex value disappears — use currentColor or system color keywords (CanvasText, Highlight) for the forced-colors branch. Test with a real screen reader, not just keyboard tabbing, because some readers move a virtual cursor without triggering :focus-visible.

Accessibility

WCAG 2.2 introduces Focus Appearance (2.4.13) which raises the bar beyond 2.4.7: the indicator must have an area at least equal to a 2px border around the bounding box and a contrast ratio of at least 3:1 against adjacent colors. Verify both the focus indicator color and the surrounding focused element color against each background. Under prefers-reduced-motion: reduce skip any transition on the indicator — jump straight to the focused state so the indicator never animates in.

References

Implementation depth

Custom focus indicators must improve the default outline, not erase it. Use focus-visible, outline-offset, or box-shadow rings while preserving a clear keyboard-only affordance.

Forced-colors mode is the deciding test. Decorative shadows may disappear, so provide an outline-based fallback and keep spacing stable so the focus ring does not push layout around controls.