Tailwind v4MIT licensed

Tailwind Focus Visible Stroke Outline for Keyboard Users

The accessibility argument of the collection, expressed as a naming convention: every hover utility here has a group-focus-visible: twin, so typing one and not the other leaves a visible hole in the class list. Focus also thickens the stroke and shortens the draw — a keyboard user is waiting on it, not exploring — and the forced-colors block stays in CSS because system keywords are not theme tokens.

Live Demo
Plain CSSTailwind v4
Try it

Nucleo: 44,000+ SVG icons in five families · All Icons Bundle $149

Nucleo · Affiliate link

Get the Nucleo bundle

The code

<section class="svgb-09 relative block w-full min-h-svh bg-svgb09-bg text-svgb09-fg font-svgb09-sans">
  <input class="peer absolute opacity-0 pointer-events-none" type="checkbox" id="svgb-09-theme">
  <label class="absolute z-[5] top-4 right-4 grid place-items-center size-11 rounded-lg border border-current/22 bg-current/6 cursor-pointer transition-[background-color,transform] duration-150 hover:bg-current/13 active:scale-95 peer-focus-visible:outline-2 peer-focus-visible:outline-current peer-focus-visible:outline-offset-[3px] peer-checked:[&_svg]:rotate-180" for="svgb-09-theme">
    <svg class="size-[18px] transition-transform duration-[400ms] ease-[cubic-bezier(0.2,0.7,0.3,1)]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true"><circle cx="12" cy="12" r="8.5"/><path d="M12 3.5a8.5 8.5 0 0 0 0 17Z" fill="currentColor" stroke="none"/></svg>
    <span class="absolute size-px overflow-hidden [clip-path:inset(50%)] whitespace-nowrap">Switch colour scheme</span>
  </label>
  <div class="min-h-svh grid place-items-center p-[clamp(20px,5vw,64px)]">
    <div class="w-full max-w-140 flex flex-col gap-3.5 p-[clamp(22px,4vw,40px)] rounded-[10px] bg-svgb09-card border border-svgb09-rest">
      <div class="flex flex-wrap justify-between items-baseline gap-3">
        <span class="text-sm font-bold tracking-[0.14em] uppercase">Meridian</span>
        <span class="inline-flex items-center gap-2 font-svgb09-mono text-[11px] text-svgb09-muted"><kbd class="font-[inherit] text-[10.5px] px-2 py-1 rounded-[5px] border border-svgb09-rest shadow-[0_1.5px_0_var(--color-svgb09-rest)]">Tab</kbd> walks the order</span>
      </div>
      <p class="mt-2 mb-0 text-[clamp(34px,6vw,52px)] font-bold tracking-[-0.03em] tabular-nums">&pound;1,480.00</p>
      <p class="mt-0 mb-2.5 text-[13.5px] leading-normal text-svgb09-muted [&_strong]:text-svgb09-fg [&_strong]:font-semibold">to <strong>Halden Studio Ltd</strong> &middot; sort 20-45-11 &middot; acct &bull;&bull;&bull;&bull;8842</p>

      <ol class="list-none m-0 p-0 flex flex-col gap-3">
        <li class="flex items-center gap-3.5">
          <span class="flex-none size-[22px] grid place-items-center rounded-full border border-svgb09-rest font-svgb09-mono text-[10.5px] text-svgb09-muted" aria-hidden="true">1</span>
          <button class="group relative w-[min(340px,100%)] h-15 grid place-items-center border-0 p-0 rounded-lg bg-svgb09-stroke/12 text-[15px] font-semibold cursor-pointer focus:outline-none focus-visible:outline-2 focus-visible:outline-svgb09-ring focus-visible:outline-offset-[3px] focus-visible:shadow-[0_0_0_5px_var(--color-svgb09-ring2)]" type="button">
            <svg class="absolute inset-0 w-full h-full overflow-visible" viewBox="0 0 340 60" aria-hidden="true">
              <rect class="fill-none stroke-svgb09-rest [stroke-width:1.4] [stroke-linecap:round]" x="1.5" y="1.5" width="337" height="57" rx="8" vector-effect="non-scaling-stroke"/>
              <rect class="fill-none stroke-svgb09-stroke [stroke-width:2] [stroke-linecap:round] [stroke-dasharray:780] [stroke-dashoffset:780] [transition:stroke-dashoffset_560ms_cubic-bezier(0.22,1,0.36,1),stroke-width_160ms_ease] group-hover:[stroke-dashoffset:0] group-focus-visible:[stroke-dashoffset:0] group-focus-visible:[stroke-width:2.8] group-focus-visible:[transition-duration:240ms] motion-reduce:transition-none motion-reduce:[stroke-dasharray:none] motion-reduce:[stroke-dashoffset:0] motion-reduce:opacity-0 motion-reduce:group-hover:opacity-100 motion-reduce:group-focus-visible:opacity-100" x="1.5" y="1.5" width="337" height="57" rx="8" vector-effect="non-scaling-stroke"/>
            </svg>
            <span class="relative z-[1]">Confirm payment</span>
          </button>
        </li>
        <li class="flex items-center gap-3.5">
          <span class="flex-none size-[22px] grid place-items-center rounded-full border border-svgb09-rest font-svgb09-mono text-[10.5px] text-svgb09-muted" aria-hidden="true">2</span>
          <button class="group relative w-[min(340px,100%)] h-15 grid place-items-center border-0 p-0 rounded-lg bg-svgb09-fg/4 text-[15px] font-medium cursor-pointer focus:outline-none focus-visible:outline-2 focus-visible:outline-svgb09-ring focus-visible:outline-offset-[3px] focus-visible:shadow-[0_0_0_5px_var(--color-svgb09-ring2)]" type="button">
            <svg class="absolute inset-0 w-full h-full overflow-visible" viewBox="0 0 340 60" aria-hidden="true">
              <rect class="fill-none stroke-svgb09-rest [stroke-width:1.4] [stroke-linecap:round]" x="1.5" y="1.5" width="337" height="57" rx="8" vector-effect="non-scaling-stroke"/>
              <rect class="fill-none stroke-svgb09-stroke [stroke-width:2] [stroke-linecap:round] [stroke-dasharray:780] [stroke-dashoffset:780] [transition:stroke-dashoffset_560ms_cubic-bezier(0.22,1,0.36,1),stroke-width_160ms_ease] group-hover:[stroke-dashoffset:0] group-focus-visible:[stroke-dashoffset:0] group-focus-visible:[stroke-width:2.8] group-focus-visible:[transition-duration:240ms] motion-reduce:transition-none motion-reduce:[stroke-dasharray:none] motion-reduce:[stroke-dashoffset:0] motion-reduce:opacity-0 motion-reduce:group-hover:opacity-100 motion-reduce:group-focus-visible:opacity-100" x="1.5" y="1.5" width="337" height="57" rx="8" vector-effect="non-scaling-stroke"/>
            </svg>
            <span class="relative z-[1]">Schedule for 1 October</span>
          </button>
        </li>
        <li class="flex items-center gap-3.5">
          <span class="flex-none size-[22px] grid place-items-center rounded-full border border-svgb09-rest font-svgb09-mono text-[10.5px] text-svgb09-muted" aria-hidden="true">3</span>
          <button class="group relative w-[min(340px,100%)] h-15 grid place-items-center border-0 p-0 rounded-lg bg-svgb09-fg/4 text-[15px] font-medium cursor-pointer focus:outline-none focus-visible:outline-2 focus-visible:outline-svgb09-ring focus-visible:outline-offset-[3px] focus-visible:shadow-[0_0_0_5px_var(--color-svgb09-ring2)]" type="button">
            <svg class="absolute inset-0 w-full h-full overflow-visible" viewBox="0 0 340 60" aria-hidden="true">
              <rect class="fill-none stroke-svgb09-rest [stroke-width:1.4] [stroke-linecap:round]" x="1.5" y="1.5" width="337" height="57" rx="8" vector-effect="non-scaling-stroke"/>
              <rect class="fill-none stroke-svgb09-stroke [stroke-width:2] [stroke-linecap:round] [stroke-dasharray:780] [stroke-dashoffset:780] [transition:stroke-dashoffset_560ms_cubic-bezier(0.22,1,0.36,1),stroke-width_160ms_ease] group-hover:[stroke-dashoffset:0] group-focus-visible:[stroke-dashoffset:0] group-focus-visible:[stroke-width:2.8] group-focus-visible:[transition-duration:240ms] motion-reduce:transition-none motion-reduce:[stroke-dasharray:none] motion-reduce:[stroke-dashoffset:0] motion-reduce:opacity-0 motion-reduce:group-hover:opacity-100 motion-reduce:group-focus-visible:opacity-100" x="1.5" y="1.5" width="337" height="57" rx="8" vector-effect="non-scaling-stroke"/>
            </svg>
            <span class="relative z-[1]">Edit reference</span>
          </button>
        </li>
        <li class="flex items-center gap-3.5">
          <span class="flex-none size-[22px] grid place-items-center rounded-full border border-svgb09-rest font-svgb09-mono text-[10.5px] text-svgb09-muted" aria-hidden="true">4</span>
          <a class="text-svgb09-stroke text-sm font-medium underline decoration-1 underline-offset-[3px] rounded hover:decoration-2 focus-visible:outline-2 focus-visible:outline-svgb09-ring focus-visible:outline-offset-[3px] focus-visible:shadow-[0_0_0_5px_var(--color-svgb09-ring2)]" href="#svgb-09-terms">Payment limits and cut-off times</a>
        </li>
      </ol>

      <div class="flex flex-wrap gap-4 pt-3.5 mt-1 border-t border-svgb09-rest">
        <span class="inline-flex items-center gap-2 font-svgb09-mono text-[10.5px] text-svgb09-muted"><span class="w-[22px] h-3 rounded-[3px] border-2 border-svgb09-stroke"></span>hover &middot; 2px stroke only</span>
        <span class="inline-flex items-center gap-2 font-svgb09-mono text-[10.5px] text-svgb09-muted"><span class="w-[22px] h-3 rounded-[3px] border-2 border-svgb09-stroke outline-2 outline-svgb09-ring outline-offset-2"></span>focus-visible &middot; outline + stroke</span>
      </div>
    </div>
  </div>
</section>
@import "tailwindcss";

@theme {
  --color-svgb09-bg: #eceef2;
  --color-svgb09-bg: oklch(0.945 0.005 265);
  --color-svgb09-fg: #0e1116;
  --color-svgb09-fg: oklch(0.175 0.008 265);
  --color-svgb09-muted: #545a66;
  --color-svgb09-muted: oklch(0.485 0.013 265);
  --color-svgb09-card: #ffffff;
  --color-svgb09-card: oklch(1 0 0);
  --color-svgb09-rest: rgba(14, 17, 22, 0.18);
  --color-svgb09-rest: oklch(0.175 0.008 265 / 0.18);
  --color-svgb09-stroke: #0b5cd6;
  --color-svgb09-stroke: oklch(0.47 0.20 260);
  --color-svgb09-ring: #0b0e14;
  --color-svgb09-ring: oklch(0.17 0.01 265);
  --color-svgb09-ring2: #ffffff;
  --color-svgb09-ring2: oklch(1 0 0);
  --font-svgb09-sans: 'Public Sans', system-ui, sans-serif;
  --font-svgb09-mono: 'JetBrains Mono', ui-monospace, monospace;
}

.svgb-09:has(#svgb-09-theme:checked) {
  --color-svgb09-bg: #0c0f14;
  --color-svgb09-bg: oklch(0.17 0.008 265);
  --color-svgb09-fg: #edeff4;
  --color-svgb09-fg: oklch(0.955 0.005 265);
  --color-svgb09-muted: #8b91a0;
  --color-svgb09-muted: oklch(0.67 0.013 265);
  --color-svgb09-card: #14171e;
  --color-svgb09-card: oklch(0.215 0.01 265);
  --color-svgb09-rest: rgba(237, 239, 244, 0.18);
  --color-svgb09-rest: oklch(0.955 0.005 265 / 0.18);
  --color-svgb09-stroke: #7fb2ff;
  --color-svgb09-stroke: oklch(0.78 0.13 260);
  --color-svgb09-ring: #ffffff;
  --color-svgb09-ring: oklch(1 0 0);
  --color-svgb09-ring2: #00080f;
  --color-svgb09-ring2: oklch(0.14 0.02 250);
}
/* Forced-colors mode discards author colours and remaps a small system palette.
   An SVG stroke is not part of that palette, so the drawn border has to be
   surrendered and a real border put back. There is no utility for this because
   the values are system keywords, not tokens. */

@media (forced-colors: active) {
  .svgb-09 svg rect {
    stroke: transparent;
  }

  .svgb-09 button {
    border: 1px solid ButtonBorder;
  }

  .svgb-09 button:focus-visible,
    .svgb-09 a:focus-visible {
    outline: 3px solid Highlight;
    outline-offset: 3px;
    box-shadow: none;
  }
}

Setup — do this once per project

Skip this if your project already runs Tailwind v4.

1Load Tailwind
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>

Compiles in the browser — perfect for a CodePen or a quick test, not for production.

2Add the stylesheetRequiredthe @theme block is what makes the custom utilities exist
@import "tailwindcss";

@theme {
  --color-svgb09-bg: #eceef2;
  --color-svgb09-bg: oklch(0.945 0.005 265);
  --color-svgb09-fg: #0e1116;
  --color-svgb09-fg: oklch(0.175 0.008 265);
  --color-svgb09-muted: #545a66;
  --color-svgb09-muted: oklch(0.485 0.013 265);
  --color-svgb09-card: #ffffff;
  --color-svgb09-card: oklch(1 0 0);
  --color-svgb09-rest: rgba(14, 17, 22, 0.18);
  --color-svgb09-rest: oklch(0.175 0.008 265 / 0.18);
  --color-svgb09-stroke: #0b5cd6;
  --color-svgb09-stroke: oklch(0.47 0.20 260);
  --color-svgb09-ring: #0b0e14;
  --color-svgb09-ring: oklch(0.17 0.01 265);
  --color-svgb09-ring2: #ffffff;
  --color-svgb09-ring2: oklch(1 0 0);
  --font-svgb09-sans: 'Public Sans', system-ui, sans-serif;
  --font-svgb09-mono: 'JetBrains Mono', ui-monospace, monospace;
}

.svgb-09:has(#svgb-09-theme:checked) {
  --color-svgb09-bg: #0c0f14;
  --color-svgb09-bg: oklch(0.17 0.008 265);
  --color-svgb09-fg: #edeff4;
  --color-svgb09-fg: oklch(0.955 0.005 265);
  --color-svgb09-muted: #8b91a0;
  --color-svgb09-muted: oklch(0.67 0.013 265);
  --color-svgb09-card: #14171e;
  --color-svgb09-card: oklch(0.215 0.01 265);
  --color-svgb09-rest: rgba(237, 239, 244, 0.18);
  --color-svgb09-rest: oklch(0.955 0.005 265 / 0.18);
  --color-svgb09-stroke: #7fb2ff;
  --color-svgb09-stroke: oklch(0.78 0.13 260);
  --color-svgb09-ring: #ffffff;
  --color-svgb09-ring: oklch(1 0 0);
  --color-svgb09-ring2: #00080f;
  --color-svgb09-ring2: oklch(0.14 0.02 250);
}
/* Forced-colors mode discards author colours and remaps a small system palette.
   An SVG stroke is not part of that palette, so the drawn border has to be
   surrendered and a real border put back. There is no utility for this because
   the values are system keywords, not tokens. */

@media (forced-colors: active) {
  .svgb-09 svg rect {
    stroke: transparent;
  }

  .svgb-09 button {
    border: 1px solid ButtonBorder;
  }

  .svgb-09 button:focus-visible,
    .svgb-09 a:focus-visible {
    outline: 3px solid Highlight;
    outline-offset: 3px;
    box-shadow: none;
  }
}
3Fonts used by this demooptional — skip if you have your own type stack
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Public+Sans:wght@400;500;700&family=JetBrains+Mono:wght@400;500&display=swap">

What changed in the port

Which parts became utilities, which stayed CSS, and why.

  • The webfonts load as <link> tags in setup, not as an @import inside the stylesheet. Tailwind emits user @import rules after its own @layer blocks, and a late @import is invalid CSS — the browser drops it and the demo silently falls back to system fonts.
  • Every hover utility has a group-focus-visible: twin, which is the demo&rsquo;s whole argument expressed as a naming convention: if you type one and not the other, the omission is visible in the markup.
  • focus:outline-none paired with focus-visible:outline-2 reproduces :focus:not(:focus-visible) — Tailwind orders focus-visible after focus, so a mouse click gets no ring and a Tab key does.
  • The keyboard ring is deliberately two-part: focus-visible:outline-svgb09-ring plus focus-visible:shadow-[0_0_0_5px_var(--color-svgb09-ring2)], so it stays visible on both the card and the primary button&rsquo;s tinted fill.
  • Focus also thickens the stroke — group-focus-visible:[stroke-width:2.8] — and shortens the draw with group-focus-visible:[transition-duration:240ms], because a keyboard user is waiting on it, not exploring.
  • The forced-colors block stays in CSS. Its values are system keywords like ButtonBorder and Highlight, which are not theme tokens and must not be.
  • The two legend swatches are pure utilities — a border for hover, a border plus an outline for focus — so the key matches the real thing by construction.
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS demo from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Tailwind Focus Visible Stroke Outline for Keyboard Users
Source: https://codefronts.com/components/tailwind-svg-stroke-buttons/tailwind-focus-visible-stroke-outline-for-keyboard-users/

The accessibility argument of the collection, expressed as a naming convention: every hover utility here has a group-focus-visible: twin, so typing one and not the other leaves a visible hole in the class list. Focus also thickens the stroke and shortens the draw &mdash; a keyboard user is waiting on it, not exploring &mdash; and the forced-colors block stays in CSS because system keywords are not theme tokens.
## HTML
```html
<section class="svgb-09 relative block w-full min-h-svh bg-svgb09-bg text-svgb09-fg font-svgb09-sans">
  <input class="peer absolute opacity-0 pointer-events-none" type="checkbox" id="svgb-09-theme">
  <label class="absolute z-[5] top-4 right-4 grid place-items-center size-11 rounded-lg border border-current/22 bg-current/6 cursor-pointer transition-[background-color,transform] duration-150 hover:bg-current/13 active:scale-95 peer-focus-visible:outline-2 peer-focus-visible:outline-current peer-focus-visible:outline-offset-[3px] peer-checked:[&_svg]:rotate-180" for="svgb-09-theme">
    <svg class="size-[18px] transition-transform duration-[400ms] ease-[cubic-bezier(0.2,0.7,0.3,1)]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true"><circle cx="12" cy="12" r="8.5"/><path d="M12 3.5a8.5 8.5 0 0 0 0 17Z" fill="currentColor" stroke="none"/></svg>
    <span class="absolute size-px overflow-hidden [clip-path:inset(50%)] whitespace-nowrap">Switch colour scheme</span>
  </label>
  <div class="min-h-svh grid place-items-center p-[clamp(20px,5vw,64px)]">
    <div class="w-full max-w-140 flex flex-col gap-3.5 p-[clamp(22px,4vw,40px)] rounded-[10px] bg-svgb09-card border border-svgb09-rest">
      <div class="flex flex-wrap justify-between items-baseline gap-3">
        <span class="text-sm font-bold tracking-[0.14em] uppercase">Meridian</span>
        <span class="inline-flex items-center gap-2 font-svgb09-mono text-[11px] text-svgb09-muted"><kbd class="font-[inherit] text-[10.5px] px-2 py-1 rounded-[5px] border border-svgb09-rest shadow-[0_1.5px_0_var(--color-svgb09-rest)]">Tab</kbd> walks the order</span>
      </div>
      <p class="mt-2 mb-0 text-[clamp(34px,6vw,52px)] font-bold tracking-[-0.03em] tabular-nums">&pound;1,480.00</p>
      <p class="mt-0 mb-2.5 text-[13.5px] leading-normal text-svgb09-muted [&_strong]:text-svgb09-fg [&_strong]:font-semibold">to <strong>Halden Studio Ltd</strong> &middot; sort 20-45-11 &middot; acct &bull;&bull;&bull;&bull;8842</p>

      <ol class="list-none m-0 p-0 flex flex-col gap-3">
        <li class="flex items-center gap-3.5">
          <span class="flex-none size-[22px] grid place-items-center rounded-full border border-svgb09-rest font-svgb09-mono text-[10.5px] text-svgb09-muted" aria-hidden="true">1</span>
          <button class="group relative w-[min(340px,100%)] h-15 grid place-items-center border-0 p-0 rounded-lg bg-svgb09-stroke/12 text-[15px] font-semibold cursor-pointer focus:outline-none focus-visible:outline-2 focus-visible:outline-svgb09-ring focus-visible:outline-offset-[3px] focus-visible:shadow-[0_0_0_5px_var(--color-svgb09-ring2)]" type="button">
            <svg class="absolute inset-0 w-full h-full overflow-visible" viewBox="0 0 340 60" aria-hidden="true">
              <rect class="fill-none stroke-svgb09-rest [stroke-width:1.4] [stroke-linecap:round]" x="1.5" y="1.5" width="337" height="57" rx="8" vector-effect="non-scaling-stroke"/>
              <rect class="fill-none stroke-svgb09-stroke [stroke-width:2] [stroke-linecap:round] [stroke-dasharray:780] [stroke-dashoffset:780] [transition:stroke-dashoffset_560ms_cubic-bezier(0.22,1,0.36,1),stroke-width_160ms_ease] group-hover:[stroke-dashoffset:0] group-focus-visible:[stroke-dashoffset:0] group-focus-visible:[stroke-width:2.8] group-focus-visible:[transition-duration:240ms] motion-reduce:transition-none motion-reduce:[stroke-dasharray:none] motion-reduce:[stroke-dashoffset:0] motion-reduce:opacity-0 motion-reduce:group-hover:opacity-100 motion-reduce:group-focus-visible:opacity-100" x="1.5" y="1.5" width="337" height="57" rx="8" vector-effect="non-scaling-stroke"/>
            </svg>
            <span class="relative z-[1]">Confirm payment</span>
          </button>
        </li>
        <li class="flex items-center gap-3.5">
          <span class="flex-none size-[22px] grid place-items-center rounded-full border border-svgb09-rest font-svgb09-mono text-[10.5px] text-svgb09-muted" aria-hidden="true">2</span>
          <button class="group relative w-[min(340px,100%)] h-15 grid place-items-center border-0 p-0 rounded-lg bg-svgb09-fg/4 text-[15px] font-medium cursor-pointer focus:outline-none focus-visible:outline-2 focus-visible:outline-svgb09-ring focus-visible:outline-offset-[3px] focus-visible:shadow-[0_0_0_5px_var(--color-svgb09-ring2)]" type="button">
            <svg class="absolute inset-0 w-full h-full overflow-visible" viewBox="0 0 340 60" aria-hidden="true">
              <rect class="fill-none stroke-svgb09-rest [stroke-width:1.4] [stroke-linecap:round]" x="1.5" y="1.5" width="337" height="57" rx="8" vector-effect="non-scaling-stroke"/>
              <rect class="fill-none stroke-svgb09-stroke [stroke-width:2] [stroke-linecap:round] [stroke-dasharray:780] [stroke-dashoffset:780] [transition:stroke-dashoffset_560ms_cubic-bezier(0.22,1,0.36,1),stroke-width_160ms_ease] group-hover:[stroke-dashoffset:0] group-focus-visible:[stroke-dashoffset:0] group-focus-visible:[stroke-width:2.8] group-focus-visible:[transition-duration:240ms] motion-reduce:transition-none motion-reduce:[stroke-dasharray:none] motion-reduce:[stroke-dashoffset:0] motion-reduce:opacity-0 motion-reduce:group-hover:opacity-100 motion-reduce:group-focus-visible:opacity-100" x="1.5" y="1.5" width="337" height="57" rx="8" vector-effect="non-scaling-stroke"/>
            </svg>
            <span class="relative z-[1]">Schedule for 1 October</span>
          </button>
        </li>
        <li class="flex items-center gap-3.5">
          <span class="flex-none size-[22px] grid place-items-center rounded-full border border-svgb09-rest font-svgb09-mono text-[10.5px] text-svgb09-muted" aria-hidden="true">3</span>
          <button class="group relative w-[min(340px,100%)] h-15 grid place-items-center border-0 p-0 rounded-lg bg-svgb09-fg/4 text-[15px] font-medium cursor-pointer focus:outline-none focus-visible:outline-2 focus-visible:outline-svgb09-ring focus-visible:outline-offset-[3px] focus-visible:shadow-[0_0_0_5px_var(--color-svgb09-ring2)]" type="button">
            <svg class="absolute inset-0 w-full h-full overflow-visible" viewBox="0 0 340 60" aria-hidden="true">
              <rect class="fill-none stroke-svgb09-rest [stroke-width:1.4] [stroke-linecap:round]" x="1.5" y="1.5" width="337" height="57" rx="8" vector-effect="non-scaling-stroke"/>
              <rect class="fill-none stroke-svgb09-stroke [stroke-width:2] [stroke-linecap:round] [stroke-dasharray:780] [stroke-dashoffset:780] [transition:stroke-dashoffset_560ms_cubic-bezier(0.22,1,0.36,1),stroke-width_160ms_ease] group-hover:[stroke-dashoffset:0] group-focus-visible:[stroke-dashoffset:0] group-focus-visible:[stroke-width:2.8] group-focus-visible:[transition-duration:240ms] motion-reduce:transition-none motion-reduce:[stroke-dasharray:none] motion-reduce:[stroke-dashoffset:0] motion-reduce:opacity-0 motion-reduce:group-hover:opacity-100 motion-reduce:group-focus-visible:opacity-100" x="1.5" y="1.5" width="337" height="57" rx="8" vector-effect="non-scaling-stroke"/>
            </svg>
            <span class="relative z-[1]">Edit reference</span>
          </button>
        </li>
        <li class="flex items-center gap-3.5">
          <span class="flex-none size-[22px] grid place-items-center rounded-full border border-svgb09-rest font-svgb09-mono text-[10.5px] text-svgb09-muted" aria-hidden="true">4</span>
          <a class="text-svgb09-stroke text-sm font-medium underline decoration-1 underline-offset-[3px] rounded hover:decoration-2 focus-visible:outline-2 focus-visible:outline-svgb09-ring focus-visible:outline-offset-[3px] focus-visible:shadow-[0_0_0_5px_var(--color-svgb09-ring2)]" href="#svgb-09-terms">Payment limits and cut-off times</a>
        </li>
      </ol>

      <div class="flex flex-wrap gap-4 pt-3.5 mt-1 border-t border-svgb09-rest">
        <span class="inline-flex items-center gap-2 font-svgb09-mono text-[10.5px] text-svgb09-muted"><span class="w-[22px] h-3 rounded-[3px] border-2 border-svgb09-stroke"></span>hover &middot; 2px stroke only</span>
        <span class="inline-flex items-center gap-2 font-svgb09-mono text-[10.5px] text-svgb09-muted"><span class="w-[22px] h-3 rounded-[3px] border-2 border-svgb09-stroke outline-2 outline-svgb09-ring outline-offset-2"></span>focus-visible &middot; outline + stroke</span>
      </div>
    </div>
  </div>
</section>
```
## CSS
```css
@import "tailwindcss";

@theme {
  --color-svgb09-bg: #eceef2;
  --color-svgb09-bg: oklch(0.945 0.005 265);
  --color-svgb09-fg: #0e1116;
  --color-svgb09-fg: oklch(0.175 0.008 265);
  --color-svgb09-muted: #545a66;
  --color-svgb09-muted: oklch(0.485 0.013 265);
  --color-svgb09-card: #ffffff;
  --color-svgb09-card: oklch(1 0 0);
  --color-svgb09-rest: rgba(14, 17, 22, 0.18);
  --color-svgb09-rest: oklch(0.175 0.008 265 / 0.18);
  --color-svgb09-stroke: #0b5cd6;
  --color-svgb09-stroke: oklch(0.47 0.20 260);
  --color-svgb09-ring: #0b0e14;
  --color-svgb09-ring: oklch(0.17 0.01 265);
  --color-svgb09-ring2: #ffffff;
  --color-svgb09-ring2: oklch(1 0 0);
  --font-svgb09-sans: 'Public Sans', system-ui, sans-serif;
  --font-svgb09-mono: 'JetBrains Mono', ui-monospace, monospace;
}

.svgb-09:has(#svgb-09-theme:checked) {
  --color-svgb09-bg: #0c0f14;
  --color-svgb09-bg: oklch(0.17 0.008 265);
  --color-svgb09-fg: #edeff4;
  --color-svgb09-fg: oklch(0.955 0.005 265);
  --color-svgb09-muted: #8b91a0;
  --color-svgb09-muted: oklch(0.67 0.013 265);
  --color-svgb09-card: #14171e;
  --color-svgb09-card: oklch(0.215 0.01 265);
  --color-svgb09-rest: rgba(237, 239, 244, 0.18);
  --color-svgb09-rest: oklch(0.955 0.005 265 / 0.18);
  --color-svgb09-stroke: #7fb2ff;
  --color-svgb09-stroke: oklch(0.78 0.13 260);
  --color-svgb09-ring: #ffffff;
  --color-svgb09-ring: oklch(1 0 0);
  --color-svgb09-ring2: #00080f;
  --color-svgb09-ring2: oklch(0.14 0.02 250);
}
/* Forced-colors mode discards author colours and remaps a small system palette.
   An SVG stroke is not part of that palette, so the drawn border has to be
   surrendered and a real border put back. There is no utility for this because
   the values are system keywords, not tokens. */

@media (forced-colors: active) {
  .svgb-09 svg rect {
    stroke: transparent;
  }

  .svgb-09 button {
    border: 1px solid ButtonBorder;
  }

  .svgb-09 button:focus-visible,
    .svgb-09 a:focus-visible {
    outline: 3px solid Highlight;
    outline-offset: 3px;
    box-shadow: none;
  }
}
```

How this works

The pairing is the pattern. A stroke that only draws on hover is invisible to anyone driving the page from a keyboard. Written as utilities, the fix is a habit you can see:

class="group-hover:[stroke-dashoffset:0]
       group-focus-visible:[stroke-dashoffset:0]
       group-hover:text-svgb09-ink
       group-focus-visible:text-svgb09-ink"

Mouse clicks should not get a ring; Tab should. focus:outline-none focus-visible:outline-2 is the utility form of :focus:not(:focus-visible). It works because Tailwind emits focus-visible after focus, so the later rule wins for keyboard focus while a pointer click keeps the suppressed outline.

The ring is deliberately two-part. One colour cannot stay visible on a white card and on the primary button's tinted fill. An outline in the ring colour plus a 5px shadow in a second colour gives a light-on-dark and dark-on-light edge at once — the same reason marching ants need two colours in demo 06.

Focus is not a slower hover. Under group-focus-visible: the stroke thickens to 2.8 and the draw drops to 240ms. Someone tabbing through a payment form is waiting for confirmation of where they are; someone hovering is browsing. The distinction costs two utilities.

What must stay CSS. The forced-colors block uses ButtonBorder, Highlight and CanvasText. These are system keywords, not design values — putting them in @theme would generate utilities that lie about what they are. SVG strokes are not remapped by forced-colors mode, which is why this block exists at all.

Make it yours

  • Retime the focus draw with group-focus-visible:[transition-duration:...]; keep it shorter than the hover draw.
  • Recolour the ring by editing the two ring tokens. Test on both the card and the tinted button before shipping — that is what the second colour is for.
  • Remove the tab-order badges for production; they are teaching aids, not UI.
  • Thicken the focus stroke further on dense layouts, where 2.8 can still get lost against a busy background.
  • Keep focus:outline-none paired with a focus-visible: rule. On its own it is one of the most common accessibility regressions on the web.

Gotchas — read before shipping

  • focus:outline-none without a focus-visible: replacement removes the only signal a keyboard user has. Never ship one without the other.
  • outline-hidden and outline-none are not the same in v4: the former preserves the outline in forced-colors mode, the latter does not.
  • A single-colour ring will disappear against some backgrounds. Two layers is the reliable answer.
  • Forced-colors keywords must not go in @theme — they are not colours you own, and the generated utilities would be misleading.
  • SVG strokes are not remapped by forced-colors mode, so a stroke-only state disappears entirely there. Carry state in wording and ARIA too.
  • Hover and focus-visible drifting apart is the failure this demo is designed to prevent; if you extract these into a component, extract the pair.

Browser support

ChromeSafariFirefoxEdge
111+16.4+128+111+

:focus-visible is supported everywhere current (Safari 15.4+), and forced-colors mode is a Windows high-contrast feature that degrades to being simply absent elsewhere. The floor is Tailwind v4's generated sheet.

Techniques used in this demo

Utilitiesfirst-class utility or variant

Search CodeFronts

Loading…