← Back to gallery
CSS

Focus Ring Pulse Spacing

A keyboard-visible focus ring that breathes outward by animating outline-offset across three production contexts — Primary Button (CTA pulse), Inline Link (tight in-prose ring), and Form Field (wider ring that clears the input border). The element box never shifts, and prefers-reduced-motion freezes the ring at its widest point so focus stays visible without motion.

focus-ringpulseoutline-offsetkeyboard-a11yno-layout-shiftprefers-reduced-motion

Focus · outline-offset · keyboard a11y

Focus Ring Pulse Spacing

A keyboard-visible focus ring that breathes outward by animating outline-offset rather than outline-width — the element box never shifts, and prefers-reduced-motion can freeze the ring at its widest point. Three product contexts each tune the spacing for their density: a Primary Button (CTA pulse), an Inline Link (tight in-prose ring), and a Form Field (wider ring that clears the input border). Stage cards force a synthetic .is-focused class so the breathing plays without input; inspector preview is a real interactive control with Tab captured to refocus the demo target.

CTA · outward breathing ring

Primary Button

A high-contrast focus ring that pulses out from the button edge using outline-offset, so the button chrome stays pinned in place. Outline width and color stay fixed for legibility; only the offset breathes between a tight and a relaxed value to give keyboard users a calm, steady heartbeat of focus.

  • button
  • :focus-visible
  • outline-offset

Inline · prose-friendly ring

Inline Link

A tighter pulse tuned for inline text links so the outline stays readable against prose without pushing neighbouring words around. Inline text rarely has room for a wide ring, so the offset stays small; the rhythm is slightly slower than the CTA to avoid flicker while scanning copy.

  • inline-link
  • text-baseline
  • small offset

Input · wider clearance ring

Form Field

A broader breathing ring paired with an editable input so focus feels confirmed even when the field already has a visible border. Inputs already carry their own border, so the focus ring needs extra offset to clear the existing chrome — the wider band also signals the active row on dense forms.

  • input
  • text-field
  • wider offset

Focus ring pulse inspector

Primary Button

press tab
  • button
  • :focus-visible
  • outline-offset

A high-contrast focus ring that pulses out from the button edge using outline-offset, so the button chrome stays pinned in place. Outline width and color stay fixed for legibility; only the offset breathes between a tight and a relaxed value to give keyboard users a calm, steady heartbeat of focus.

Helped you ship something? 🐟 Send my cat a churu

/* Outward breathing ring on a CTA button — outline-offset pulses 2 → 6px. */
.focus-ring-target:focus-visible {
  outline: 2px solid #7dd3fc;
  outline-offset: 2px;
  animation: focusRingPulse 1.8s ease-in-out infinite;
}

@keyframes focusRingPulse {
  0%, 100% { outline-offset: 2px; }
  50%      { outline-offset: 6px; }
}

@media (prefers-reduced-motion: reduce) {
  .focus-ring-target:focus-visible {
    animation: none;
    outline-offset: 6px;       /* freeze on the wide value */
  }
}

How to make this

A focus ring pulse keeps outline width constant, animates outline-offset on :focus-visible, and freezes at the widest offset for reduced motion.

html
1<button class="focus-pulse-button" type="button">  Continue</button> <style>.focus-pulse-button {  border: 1px solid rgba(125, 211, 252, .34);  border-radius: 999px;  padding: .72rem 1.2rem;  color: #f8fafc;  background: #0f172a;  font: 700 .95rem/1 ui-sans-serif, system-ui;  cursor: pointer;14  outline: 0 solid transparent;  outline-offset: 0;}17.focus-pulse-button:focus-visible {  outline: 2px solid #7dd3fc;19  outline-offset: 2px;20  animation: focus-ring-recipe-pulse 1.8s ease-in-out infinite;}@keyframes focus-ring-recipe-pulse {  0%, 100% { outline-offset: 2px; }  50% { outline-offset: 7px; }}26@media (prefers-reduced-motion: reduce) {  .focus-pulse-button:focus-visible {    animation: none;    outline-offset: 7px;  }}</style>

Annotated snippet

  1. Line 1Use a real interactive element so keyboard focus, disabled state, and activation behavior already exist. The pulse should enhance focus, not simulate it.
  2. Line 14The base state resets outline without removing focus styling forever. The visible ring is reintroduced only for :focus-visible.

    Both buttons are in their focused state. Removing the default outline without adding a custom one leaves keyboard users with no focus indicator at all; the same outline:none paired with a :focus-visible ring keeps the focus state visible.

    PitfallCan I remove the default outline for a custom focus ring?

    Only if you replace it with an equally visible focus style. Reset the base outline carefully, then define a high-contrast :focus-visible state with enough offset to clear borders and shadows.

  3. Line 17:focus-visible avoids pulsing after every mouse click while still giving keyboard users a clear focus target.
    PitfallShould focus rings pulse on mouse click?

    Usually no. Use :focus-visible so keyboard and assistive-technology focus gets the strong indicator while pointer clicks do not trigger unnecessary motion.

  4. Line 19Keep outline width fixed and animate spacing with outline-offset. This preserves a consistent ring thickness while the breathing motion moves outward.

    Pulsing outline width changes ring weight; pulsing outline-offset keeps the ring legible and moves only the spacing.

    PitfallWhy animate outline-offset instead of outline-width?

    A changing outline width alters visual weight and can crowd nearby UI. A fixed width with changing offset keeps the ring readable while creating the breathing effect through spacing.

  5. Line 20The animation only runs while the element is actually focus-visible. Do not run decorative focus motion when nothing is focused.
    PitfallShould focus rings pulse on mouse click?

    Usually no. Use :focus-visible so keyboard and assistive-technology focus gets the strong indicator while pointer clicks do not trigger unnecessary motion.

  6. Line 26Reduced motion freezes the widest offset rather than removing the ring. The user still gets a strong, WCAG-friendly focus indicator.
    PitfallWhat should reduced motion do for focus pulses?

    Stop the pulse and keep a static, visible focus ring at the widest useful offset. Never remove focus indication because of a motion preference.

Other pitfalls

Is outline-offset animation supported everywhere?
Modern browsers support outline-offset, but rendering can vary around complex border radii. Test dense controls and provide a static outline fallback that remains visible.

Notes

Overview

A pulsing focus ring breathes its outline-offset outward and back while focus is held. The element box never shifts; only the outline radius changes. Three production contexts ship: a primary button CTA, a tight inline-link ring, and a wider form-field ring that clears the input border.

When to use it

Reach for pulsing focus on hero CTAs and marketing surfaces where the focused element should pull attention — a loud signal that “this is where keyboard input lands next.” Skip it for dense forms or list-row focus — every focused row pulsing simultaneously is exhausting. Always pair with :focus-visible so mouse clicks do not trigger the pulse.

How it works

The element gets a static outline on :focus-visible — a fixed color and width so the visible “border” never changes. A @keyframes pulse rule then animates only outline-offset between two values (e.g. 2px to 6px), which expands the gap between the outline and the element edge without triggering layout. The outline lives outside the box model entirely, so neighbors do not shift even though the ring breathes. Pair with animation-direction: alternate for the in-and-out breathing, or chain explicit keyframes for an asymmetric ease.

Production gotchas

Using box-shadow instead of outline looks similar in light mode but disappears entirely in forced-colors mode — stick with outline for the actual ring and keep any box-shadow as a decorative halo only. The pulse animation must stop when focus leaves, but CSS animations do not auto-cancel mid-cycle; rely on the :focus-visible selector dropping to halt the rule, and write the keyframes so the resting state at 100% matches the static outline. Wide pulses on tightly-packed inputs can overlap adjacent fields visually — cap the maximum offset at the gap between fields.

Accessibility

The static focused state must already meet WCAG 2.4.13 on its own — the pulse is decoration, not the indicator. Under prefers-reduced-motion: reduce set animation: none on the pulse so the static outline remains and the breathing motion stops entirely. Avoid pulses faster than ~3Hz to stay under the photosensitivity threshold; aim for a 1.4–2 second full breath cycle.

References

Implementation depth

A pulsing focus ring should communicate location without moving the element box. Animate outline-offset or a shadow layer around the control, then keep the component dimensions fixed.

Focus is an accessibility signal, so never make it subtle just to preserve aesthetics. Reduced motion can freeze the ring at its widest clear state while still giving keyboard users a strong target.