19 / 20
All 20 in 20 Tailwind SVG Stroke Buttons
- 01Tailwind SVG Stroke Draw Button Border on Hover
- 02Tailwind Stroke Dashoffset Button Outline Animation
- 03Tailwind Corner Start Perimeter Draw Button
- 04Tailwind Two Path Split Border Draw Button
- 05Tailwind Rounded Rect Stroke Button with rx Radius
- 06Tailwind Dashed Marching Ants Button Border
- 07Tailwind Gradient Stroke Button with SVG linearGradient
- 08Tailwind Stroke Draw Button That Reverses on Mouse Leave
- 09Tailwind Focus Visible Stroke Outline for Keyboard Users
- 10Tailwind Non Scaling Stroke Button That Resizes with Text
- 11Tailwind Stroke Draw Button with pathLength Normalization
- 12Tailwind Dual Layer Stroke Button with Offset Trailing Path
- 13Tailwind Sequential Stroke and Label Fade Button
- 14Tailwind Icon Plus Border Stroke Draw Together
- 15Tailwind Stroke Draw Submit Button Loading State
- 16Tailwind Stroke Draw Button Disabled and Error States
- 17Tailwind Scroll Triggered Button Stroke with animation-timeline
- 18Tailwind Stroke Draw Button Group with Staggered Delay
- 19Tailwind Reduced Motion Fallback for Stroke Buttons
- 20Tailwind Variable Width Stroke Button with Text Length
Tailwind Reduced Motion Fallback for Stroke Buttons
The best argument for utilities in the whole collection: Tailwind's motion-reduce: variant is @media (prefers-reduced-motion: reduce), so the fallback lives in the markup next to the thing it replaces — you cannot ship the animation and forget the fallback in another file. And the fallback restates the end state rather than deleting the transition, because a bare transition-none leaves a button that never responds at all.
Nucleo: 44,000+ SVG icons in five families · All Icons Bundle $149
Nucleo · Affiliate link
The code
<section class="svgb-19 relative block w-full min-h-svh bg-svgb19-bg text-svgb19-fg font-svgb19-sans">
<input class="peer absolute opacity-0 pointer-events-none" type="checkbox" id="svgb-19-theme">
<label class="absolute z-[5] top-4 right-4 grid place-items-center size-11 rounded-full border border-current/20 bg-current/7 cursor-pointer transition-[background-color,transform] duration-150 hover:bg-current/14 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-19-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(16px,4vw,56px)]">
<div class="w-full max-w-235 grid grid-cols-[minmax(0,200px)_1fr] rounded-[14px] overflow-hidden bg-svgb19-win border border-svgb19-rest shadow-[0_1px_2px_oklch(0_0_0/0.06),0_30px_70px_-50px_oklch(0_0_0/0.4)]">
<div class="flex flex-col gap-0.5 p-2.5 bg-svgb19-side border-e border-svgb19-rest">
<span class="px-2.5 pt-1.5 pb-3 text-xs font-semibold tracking-[0.1em] uppercase text-svgb19-muted">Accessibility</span>
<span class="px-2.5 py-[9px] rounded-[7px] text-sm text-svgb19-muted">Display</span>
<span class="px-2.5 py-[9px] rounded-[7px] text-sm bg-svgb19-stroke/12 text-svgb19-fg font-medium">Motion</span>
<span class="px-2.5 py-[9px] rounded-[7px] text-sm text-svgb19-muted">Pointer</span>
<span class="px-2.5 py-[9px] rounded-[7px] text-sm text-svgb19-muted">Captions</span>
<span class="px-2.5 py-[9px] rounded-[7px] text-sm text-svgb19-muted">Audio</span>
</div>
<div class="flex flex-col gap-4 p-[clamp(20px,3.5vw,34px)]">
<h1 class="m-0 text-[clamp(24px,3.4vw,32px)] font-semibold tracking-[-0.025em]">Motion</h1>
<div class="border border-svgb19-rest rounded-xl">
<input class="peer/sim absolute opacity-0 pointer-events-none" type="checkbox" id="svgb-19-sim">
<label class="flex items-center justify-between gap-5 px-[18px] py-4 cursor-pointer peer-focus-visible/sim:outline-2 peer-focus-visible/sim:outline-svgb19-stroke peer-focus-visible/sim:-outline-offset-2 peer-focus-visible/sim:rounded-xl peer-checked/sim:[&>span:last-child]:bg-svgb19-stroke peer-checked/sim:[&>span:last-child>span]:translate-x-5" for="svgb-19-sim">
<span class="flex flex-col gap-[3px]">
<span class="text-[15px] font-medium">Reduce motion</span>
<span class="text-[13px] text-svgb19-muted">Replaces animated transitions with instant state changes.</span>
</span>
<span class="relative flex-none w-[50px] h-[30px] rounded-full bg-svgb19-fg/16 transition-[background-color] duration-200 motion-reduce:transition-none" aria-hidden="true"><span class="absolute top-[3px] left-[3px] size-6 rounded-full [background:var(--svgb-19-knob)] shadow-[0_1px_3px_oklch(0_0_0/0.3)] transition-[translate] duration-[220ms] ease-[cubic-bezier(0.22,1,0.36,1)] motion-reduce:transition-none"></span></span>
</label>
</div>
<p class="mt-1.5 mb-0 font-svgb19-mono text-[10.5px] tracking-[0.16em] uppercase text-svgb19-muted">Preview</p>
<div class="grid gap-[clamp(12px,2vw,18px)] grid-cols-[repeat(auto-fit,minmax(min(100%,236px),1fr))]">
<div class="flex flex-col gap-[9px]">
<button class="group relative w-full max-w-59 h-15 grid place-items-center border-0 p-0 rounded-xl bg-svgb19-fg/4 text-[14.5px] font-medium cursor-pointer overflow-hidden focus-visible:outline-2 focus-visible:outline-svgb19-stroke focus-visible:outline-offset-[3px]" type="button">
<svg class="absolute inset-0 w-full h-full overflow-visible [&_rect]:fill-none [&_rect]:[stroke-linecap:round]" viewBox="0 0 236 60" aria-hidden="true">
<rect class="stroke-svgb19-rest [stroke-width:1.4]" x="2" y="2" width="232" height="56" rx="12" pathLength="1"/>
<rect class="svgb-19__ink stroke-svgb19-stroke [stroke-width:2.2] [stroke-dasharray:1] [stroke-dashoffset:1] transition-[stroke-dashoffset] duration-[640ms] ease-[cubic-bezier(0.22,1,0.36,1)] group-hover:[stroke-dashoffset:0] group-focus-visible:[stroke-dashoffset:0] 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="2" y="2" width="232" height="56" rx="12" pathLength="1"/>
</svg>
<span class="svgb-19__label relative z-[1] transition-[translate,color] duration-[320ms] ease-[cubic-bezier(0.22,1,0.36,1)]">Perimeter draw</span>
</button>
<span class="font-svgb19-mono text-[10.5px] text-svgb19-muted">travel → instant presence</span>
</div>
<div class="flex flex-col gap-[9px]">
<button class="group relative w-full max-w-59 h-15 grid place-items-center border-0 p-0 rounded-xl bg-svgb19-fg/4 text-[14.5px] font-medium cursor-pointer overflow-hidden focus-visible:outline-2 focus-visible:outline-svgb19-stroke focus-visible:outline-offset-[3px]" type="button">
<svg class="absolute inset-0 w-full h-full overflow-visible [&_rect]:fill-none [&_rect]:[stroke-linecap:round]" viewBox="0 0 236 60" aria-hidden="true">
<rect class="stroke-svgb19-rest [stroke-width:1.4]" x="2" y="2" width="232" height="56" rx="12" pathLength="1"/>
<rect class="svgb-19__ink--march stroke-svgb19-stroke [stroke-width:2.2] [stroke-dasharray:0.05_0.05] [stroke-dashoffset:0] opacity-0 transition-opacity duration-200 animate-svgb19-march [animation-play-state:paused] group-hover:opacity-100 group-hover:[animation-play-state:running] group-focus-visible:opacity-100 group-focus-visible:[animation-play-state:running] motion-reduce:animate-none" x="2" y="2" width="232" height="56" rx="12" pathLength="1"/>
</svg>
<span class="svgb-19__label relative z-[1] transition-[translate,color] duration-[320ms] ease-[cubic-bezier(0.22,1,0.36,1)]">Marching dash</span>
</button>
<span class="font-svgb19-mono text-[10.5px] text-svgb19-muted">loop → static dash</span>
</div>
<div class="flex flex-col gap-[9px]">
<button class="group relative w-full max-w-59 h-15 grid place-items-center border-0 p-0 rounded-xl bg-svgb19-fg/4 text-[14.5px] font-medium cursor-pointer overflow-hidden focus-visible:outline-2 focus-visible:outline-svgb19-stroke focus-visible:outline-offset-[3px]" type="button">
<svg class="absolute inset-0 w-full h-full overflow-visible [&_rect]:fill-none [&_rect]:[stroke-linecap:round]" viewBox="0 0 236 60" aria-hidden="true">
<rect class="stroke-svgb19-rest [stroke-width:1.4]" x="2" y="2" width="232" height="56" rx="12" pathLength="1"/>
<rect class="svgb-19__ink stroke-svgb19-stroke [stroke-width:2.2] [stroke-dasharray:1] [stroke-dashoffset:1] transition-[stroke-dashoffset] duration-[640ms] ease-[cubic-bezier(0.22,1,0.36,1)] group-hover:[stroke-dashoffset:0] group-focus-visible:[stroke-dashoffset:0] 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="2" y="2" width="232" height="56" rx="12" pathLength="1"/>
</svg>
<span class="svgb-19__label relative z-[1] transition-[translate,color] duration-[320ms] ease-[cubic-bezier(0.22,1,0.36,1)] group-hover:-translate-y-0.5 group-hover:text-svgb19-stroke group-focus-visible:-translate-y-0.5 group-focus-visible:text-svgb19-stroke motion-reduce:transition-[color] motion-reduce:group-hover:translate-y-0 motion-reduce:group-focus-visible:translate-y-0">Sliding label</span>
</button>
<span class="font-svgb19-mono text-[10.5px] text-svgb19-muted">slide → colour only</span>
</div>
</div>
<p class="mt-1 mb-0 flex gap-[9px] items-baseline text-[12.5px] text-svgb19-muted"><span class="flex-none font-svgb19-mono text-[10px] px-[7px] py-[3px] rounded-[5px] bg-svgb19-stroke/12 text-[color-mix(in_oklab,var(--color-svgb19-stroke)_78%,var(--color-svgb19-fg))]">WCAG 2.3.3</span> Motion from interaction can be disabled, unless essential.</p>
</div>
</div>
</div>
</section><section class="svgb-19 relative block w-full min-h-svh bg-svgb19-bg text-svgb19-fg font-svgb19-sans">
<input class="peer absolute opacity-0 pointer-events-none" type="checkbox" id="svgb-19-theme">
<label class="absolute z-[5] top-4 right-4 grid place-items-center size-11 rounded-full border border-current/20 bg-current/7 cursor-pointer transition-[background-color,transform] duration-150 hover:bg-current/14 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-19-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(16px,4vw,56px)]">
<div class="w-full max-w-235 grid grid-cols-[minmax(0,200px)_1fr] rounded-[14px] overflow-hidden bg-svgb19-win border border-svgb19-rest shadow-[0_1px_2px_oklch(0_0_0/0.06),0_30px_70px_-50px_oklch(0_0_0/0.4)]">
<div class="flex flex-col gap-0.5 p-2.5 bg-svgb19-side border-e border-svgb19-rest">
<span class="px-2.5 pt-1.5 pb-3 text-xs font-semibold tracking-[0.1em] uppercase text-svgb19-muted">Accessibility</span>
<span class="px-2.5 py-[9px] rounded-[7px] text-sm text-svgb19-muted">Display</span>
<span class="px-2.5 py-[9px] rounded-[7px] text-sm bg-svgb19-stroke/12 text-svgb19-fg font-medium">Motion</span>
<span class="px-2.5 py-[9px] rounded-[7px] text-sm text-svgb19-muted">Pointer</span>
<span class="px-2.5 py-[9px] rounded-[7px] text-sm text-svgb19-muted">Captions</span>
<span class="px-2.5 py-[9px] rounded-[7px] text-sm text-svgb19-muted">Audio</span>
</div>
<div class="flex flex-col gap-4 p-[clamp(20px,3.5vw,34px)]">
<h1 class="m-0 text-[clamp(24px,3.4vw,32px)] font-semibold tracking-[-0.025em]">Motion</h1>
<div class="border border-svgb19-rest rounded-xl">
<input class="peer/sim absolute opacity-0 pointer-events-none" type="checkbox" id="svgb-19-sim">
<label class="flex items-center justify-between gap-5 px-[18px] py-4 cursor-pointer peer-focus-visible/sim:outline-2 peer-focus-visible/sim:outline-svgb19-stroke peer-focus-visible/sim:-outline-offset-2 peer-focus-visible/sim:rounded-xl peer-checked/sim:[&>span:last-child]:bg-svgb19-stroke peer-checked/sim:[&>span:last-child>span]:translate-x-5" for="svgb-19-sim">
<span class="flex flex-col gap-[3px]">
<span class="text-[15px] font-medium">Reduce motion</span>
<span class="text-[13px] text-svgb19-muted">Replaces animated transitions with instant state changes.</span>
</span>
<span class="relative flex-none w-[50px] h-[30px] rounded-full bg-svgb19-fg/16 transition-[background-color] duration-200 motion-reduce:transition-none" aria-hidden="true"><span class="absolute top-[3px] left-[3px] size-6 rounded-full [background:var(--svgb-19-knob)] shadow-[0_1px_3px_oklch(0_0_0/0.3)] transition-[translate] duration-[220ms] ease-[cubic-bezier(0.22,1,0.36,1)] motion-reduce:transition-none"></span></span>
</label>
</div>
<p class="mt-1.5 mb-0 font-svgb19-mono text-[10.5px] tracking-[0.16em] uppercase text-svgb19-muted">Preview</p>
<div class="grid gap-[clamp(12px,2vw,18px)] grid-cols-[repeat(auto-fit,minmax(min(100%,236px),1fr))]">
<div class="flex flex-col gap-[9px]">
<button class="group relative w-full max-w-59 h-15 grid place-items-center border-0 p-0 rounded-xl bg-svgb19-fg/4 text-[14.5px] font-medium cursor-pointer overflow-hidden focus-visible:outline-2 focus-visible:outline-svgb19-stroke focus-visible:outline-offset-[3px]" type="button">
<svg class="absolute inset-0 w-full h-full overflow-visible [&_rect]:fill-none [&_rect]:[stroke-linecap:round]" viewBox="0 0 236 60" aria-hidden="true">
<rect class="stroke-svgb19-rest [stroke-width:1.4]" x="2" y="2" width="232" height="56" rx="12" pathLength="1"/>
<rect class="svgb-19__ink stroke-svgb19-stroke [stroke-width:2.2] [stroke-dasharray:1] [stroke-dashoffset:1] transition-[stroke-dashoffset] duration-[640ms] ease-[cubic-bezier(0.22,1,0.36,1)] group-hover:[stroke-dashoffset:0] group-focus-visible:[stroke-dashoffset:0] 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="2" y="2" width="232" height="56" rx="12" pathLength="1"/>
</svg>
<span class="svgb-19__label relative z-[1] transition-[translate,color] duration-[320ms] ease-[cubic-bezier(0.22,1,0.36,1)]">Perimeter draw</span>
</button>
<span class="font-svgb19-mono text-[10.5px] text-svgb19-muted">travel → instant presence</span>
</div>
<div class="flex flex-col gap-[9px]">
<button class="group relative w-full max-w-59 h-15 grid place-items-center border-0 p-0 rounded-xl bg-svgb19-fg/4 text-[14.5px] font-medium cursor-pointer overflow-hidden focus-visible:outline-2 focus-visible:outline-svgb19-stroke focus-visible:outline-offset-[3px]" type="button">
<svg class="absolute inset-0 w-full h-full overflow-visible [&_rect]:fill-none [&_rect]:[stroke-linecap:round]" viewBox="0 0 236 60" aria-hidden="true">
<rect class="stroke-svgb19-rest [stroke-width:1.4]" x="2" y="2" width="232" height="56" rx="12" pathLength="1"/>
<rect class="svgb-19__ink--march stroke-svgb19-stroke [stroke-width:2.2] [stroke-dasharray:0.05_0.05] [stroke-dashoffset:0] opacity-0 transition-opacity duration-200 animate-svgb19-march [animation-play-state:paused] group-hover:opacity-100 group-hover:[animation-play-state:running] group-focus-visible:opacity-100 group-focus-visible:[animation-play-state:running] motion-reduce:animate-none" x="2" y="2" width="232" height="56" rx="12" pathLength="1"/>
</svg>
<span class="svgb-19__label relative z-[1] transition-[translate,color] duration-[320ms] ease-[cubic-bezier(0.22,1,0.36,1)]">Marching dash</span>
</button>
<span class="font-svgb19-mono text-[10.5px] text-svgb19-muted">loop → static dash</span>
</div>
<div class="flex flex-col gap-[9px]">
<button class="group relative w-full max-w-59 h-15 grid place-items-center border-0 p-0 rounded-xl bg-svgb19-fg/4 text-[14.5px] font-medium cursor-pointer overflow-hidden focus-visible:outline-2 focus-visible:outline-svgb19-stroke focus-visible:outline-offset-[3px]" type="button">
<svg class="absolute inset-0 w-full h-full overflow-visible [&_rect]:fill-none [&_rect]:[stroke-linecap:round]" viewBox="0 0 236 60" aria-hidden="true">
<rect class="stroke-svgb19-rest [stroke-width:1.4]" x="2" y="2" width="232" height="56" rx="12" pathLength="1"/>
<rect class="svgb-19__ink stroke-svgb19-stroke [stroke-width:2.2] [stroke-dasharray:1] [stroke-dashoffset:1] transition-[stroke-dashoffset] duration-[640ms] ease-[cubic-bezier(0.22,1,0.36,1)] group-hover:[stroke-dashoffset:0] group-focus-visible:[stroke-dashoffset:0] 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="2" y="2" width="232" height="56" rx="12" pathLength="1"/>
</svg>
<span class="svgb-19__label relative z-[1] transition-[translate,color] duration-[320ms] ease-[cubic-bezier(0.22,1,0.36,1)] group-hover:-translate-y-0.5 group-hover:text-svgb19-stroke group-focus-visible:-translate-y-0.5 group-focus-visible:text-svgb19-stroke motion-reduce:transition-[color] motion-reduce:group-hover:translate-y-0 motion-reduce:group-focus-visible:translate-y-0">Sliding label</span>
</button>
<span class="font-svgb19-mono text-[10.5px] text-svgb19-muted">slide → colour only</span>
</div>
</div>
<p class="mt-1 mb-0 flex gap-[9px] items-baseline text-[12.5px] text-svgb19-muted"><span class="flex-none font-svgb19-mono text-[10px] px-[7px] py-[3px] rounded-[5px] bg-svgb19-stroke/12 text-[color-mix(in_oklab,var(--color-svgb19-stroke)_78%,var(--color-svgb19-fg))]">WCAG 2.3.3</span> Motion from interaction can be disabled, unless essential.</p>
</div>
</div>
</div>
</section>@import "tailwindcss";
@theme {
--color-svgb19-bg: #e7e9ec;
--color-svgb19-bg: oklch(0.935 0.005 250);
--color-svgb19-fg: #12151a;
--color-svgb19-fg: oklch(0.19 0.007 250);
--color-svgb19-muted: #5b616b;
--color-svgb19-muted: oklch(0.505 0.012 250);
--color-svgb19-win: #ffffff;
--color-svgb19-win: oklch(1 0 0);
--color-svgb19-side: #f2f3f6;
--color-svgb19-side: oklch(0.965 0.004 250);
--color-svgb19-rest: rgba(18, 21, 26, 0.15);
--color-svgb19-rest: oklch(0.19 0.007 250 / 0.15);
--color-svgb19-stroke: #1a63d8;
--color-svgb19-stroke: oklch(0.50 0.20 260);
--font-svgb19-sans: 'Public Sans', system-ui, sans-serif;
--font-svgb19-mono: 'JetBrains Mono', ui-monospace, monospace;
--animate-svgb19-march: svgb19-march 900ms linear infinite;
}
@keyframes svgb19-march {
to {
stroke-dashoffset: -0.1;
}
}
.svgb-19 {
--svgb-19-knob: #ffffff;
}
/* The real fallback is in the markup, as motion-reduce:* utilities — that
variant IS this media query. What cannot be a variant is a *simulated*
setting, so the switch re-applies the identical rules through :has(), which
is what makes the fallback reviewable without changing OS preferences. */
.svgb-19:has(#svgb-19-sim:checked) .svgb-19__ink {
transition: none;
stroke-dasharray: none;
stroke-dashoffset: 0;
opacity: 0;
}
.svgb-19:has(#svgb-19-sim:checked) button:hover .svgb-19__ink,
.svgb-19:has(#svgb-19-sim:checked) button:focus-visible .svgb-19__ink {
opacity: 1;
}
.svgb-19:has(#svgb-19-sim:checked) .svgb-19__ink--march {
animation: none;
stroke-dasharray: 0.05 0.05;
}
.svgb-19:has(#svgb-19-sim:checked) .svgb-19__label {
transition: color 220ms ease;
}
.svgb-19:has(#svgb-19-sim:checked) button:hover .svgb-19__label,
.svgb-19:has(#svgb-19-sim:checked) button:focus-visible .svgb-19__label {
translate: 0 0;
}
.svgb-19:has(#svgb-19-theme:checked) {
--color-svgb19-bg: #0b0d11;
--color-svgb19-bg: oklch(0.165 0.007 250);
--color-svgb19-fg: #edeff3;
--color-svgb19-fg: oklch(0.955 0.004 250);
--color-svgb19-muted: #8c929c;
--color-svgb19-muted: oklch(0.67 0.012 250);
--color-svgb19-win: #15181e;
--color-svgb19-win: oklch(0.22 0.009 250);
--color-svgb19-side: #11141a;
--color-svgb19-side: oklch(0.19 0.008 250);
--color-svgb19-rest: rgba(237, 239, 243, 0.16);
--color-svgb19-rest: oklch(0.955 0.004 250 / 0.16);
--color-svgb19-stroke: #85b4ff;
--color-svgb19-stroke: oklch(0.79 0.13 262);
--svgb-19-knob: #0d1014;
}@import "tailwindcss";
@theme {
--color-svgb19-bg: #e7e9ec;
--color-svgb19-bg: oklch(0.935 0.005 250);
--color-svgb19-fg: #12151a;
--color-svgb19-fg: oklch(0.19 0.007 250);
--color-svgb19-muted: #5b616b;
--color-svgb19-muted: oklch(0.505 0.012 250);
--color-svgb19-win: #ffffff;
--color-svgb19-win: oklch(1 0 0);
--color-svgb19-side: #f2f3f6;
--color-svgb19-side: oklch(0.965 0.004 250);
--color-svgb19-rest: rgba(18, 21, 26, 0.15);
--color-svgb19-rest: oklch(0.19 0.007 250 / 0.15);
--color-svgb19-stroke: #1a63d8;
--color-svgb19-stroke: oklch(0.50 0.20 260);
--font-svgb19-sans: 'Public Sans', system-ui, sans-serif;
--font-svgb19-mono: 'JetBrains Mono', ui-monospace, monospace;
--animate-svgb19-march: svgb19-march 900ms linear infinite;
}
@keyframes svgb19-march {
to {
stroke-dashoffset: -0.1;
}
}
.svgb-19 {
--svgb-19-knob: #ffffff;
}
/* The real fallback is in the markup, as motion-reduce:* utilities — that
variant IS this media query. What cannot be a variant is a *simulated*
setting, so the switch re-applies the identical rules through :has(), which
is what makes the fallback reviewable without changing OS preferences. */
.svgb-19:has(#svgb-19-sim:checked) .svgb-19__ink {
transition: none;
stroke-dasharray: none;
stroke-dashoffset: 0;
opacity: 0;
}
.svgb-19:has(#svgb-19-sim:checked) button:hover .svgb-19__ink,
.svgb-19:has(#svgb-19-sim:checked) button:focus-visible .svgb-19__ink {
opacity: 1;
}
.svgb-19:has(#svgb-19-sim:checked) .svgb-19__ink--march {
animation: none;
stroke-dasharray: 0.05 0.05;
}
.svgb-19:has(#svgb-19-sim:checked) .svgb-19__label {
transition: color 220ms ease;
}
.svgb-19:has(#svgb-19-sim:checked) button:hover .svgb-19__label,
.svgb-19:has(#svgb-19-sim:checked) button:focus-visible .svgb-19__label {
translate: 0 0;
}
.svgb-19:has(#svgb-19-theme:checked) {
--color-svgb19-bg: #0b0d11;
--color-svgb19-bg: oklch(0.165 0.007 250);
--color-svgb19-fg: #edeff3;
--color-svgb19-fg: oklch(0.955 0.004 250);
--color-svgb19-muted: #8c929c;
--color-svgb19-muted: oklch(0.67 0.012 250);
--color-svgb19-win: #15181e;
--color-svgb19-win: oklch(0.22 0.009 250);
--color-svgb19-side: #11141a;
--color-svgb19-side: oklch(0.19 0.008 250);
--color-svgb19-rest: rgba(237, 239, 243, 0.16);
--color-svgb19-rest: oklch(0.955 0.004 250 / 0.16);
--color-svgb19-stroke: #85b4ff;
--color-svgb19-stroke: oklch(0.79 0.13 262);
--svgb-19-knob: #0d1014;
}Setup — do this once per project
Skip this if your project already runs Tailwind v4.
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<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.
npm install tailwindcss @tailwindcss/vite
npm install tailwindcss @tailwindcss/vite// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})Any v4 setup works — Vite, PostCSS or the CLI. Nothing in this demo is bundler-specific.
@import "tailwindcss";
@theme {
--color-svgb19-bg: #e7e9ec;
--color-svgb19-bg: oklch(0.935 0.005 250);
--color-svgb19-fg: #12151a;
--color-svgb19-fg: oklch(0.19 0.007 250);
--color-svgb19-muted: #5b616b;
--color-svgb19-muted: oklch(0.505 0.012 250);
--color-svgb19-win: #ffffff;
--color-svgb19-win: oklch(1 0 0);
--color-svgb19-side: #f2f3f6;
--color-svgb19-side: oklch(0.965 0.004 250);
--color-svgb19-rest: rgba(18, 21, 26, 0.15);
--color-svgb19-rest: oklch(0.19 0.007 250 / 0.15);
--color-svgb19-stroke: #1a63d8;
--color-svgb19-stroke: oklch(0.50 0.20 260);
--font-svgb19-sans: 'Public Sans', system-ui, sans-serif;
--font-svgb19-mono: 'JetBrains Mono', ui-monospace, monospace;
--animate-svgb19-march: svgb19-march 900ms linear infinite;
}
@keyframes svgb19-march {
to {
stroke-dashoffset: -0.1;
}
}
.svgb-19 {
--svgb-19-knob: #ffffff;
}
/* The real fallback is in the markup, as motion-reduce:* utilities — that
variant IS this media query. What cannot be a variant is a *simulated*
setting, so the switch re-applies the identical rules through :has(), which
is what makes the fallback reviewable without changing OS preferences. */
.svgb-19:has(#svgb-19-sim:checked) .svgb-19__ink {
transition: none;
stroke-dasharray: none;
stroke-dashoffset: 0;
opacity: 0;
}
.svgb-19:has(#svgb-19-sim:checked) button:hover .svgb-19__ink,
.svgb-19:has(#svgb-19-sim:checked) button:focus-visible .svgb-19__ink {
opacity: 1;
}
.svgb-19:has(#svgb-19-sim:checked) .svgb-19__ink--march {
animation: none;
stroke-dasharray: 0.05 0.05;
}
.svgb-19:has(#svgb-19-sim:checked) .svgb-19__label {
transition: color 220ms ease;
}
.svgb-19:has(#svgb-19-sim:checked) button:hover .svgb-19__label,
.svgb-19:has(#svgb-19-sim:checked) button:focus-visible .svgb-19__label {
translate: 0 0;
}
.svgb-19:has(#svgb-19-theme:checked) {
--color-svgb19-bg: #0b0d11;
--color-svgb19-bg: oklch(0.165 0.007 250);
--color-svgb19-fg: #edeff3;
--color-svgb19-fg: oklch(0.955 0.004 250);
--color-svgb19-muted: #8c929c;
--color-svgb19-muted: oklch(0.67 0.012 250);
--color-svgb19-win: #15181e;
--color-svgb19-win: oklch(0.22 0.009 250);
--color-svgb19-side: #11141a;
--color-svgb19-side: oklch(0.19 0.008 250);
--color-svgb19-rest: rgba(237, 239, 243, 0.16);
--color-svgb19-rest: oklch(0.955 0.004 250 / 0.16);
--color-svgb19-stroke: #85b4ff;
--color-svgb19-stroke: oklch(0.79 0.13 262);
--svgb-19-knob: #0d1014;
}@import "tailwindcss";
@theme {
--color-svgb19-bg: #e7e9ec;
--color-svgb19-bg: oklch(0.935 0.005 250);
--color-svgb19-fg: #12151a;
--color-svgb19-fg: oklch(0.19 0.007 250);
--color-svgb19-muted: #5b616b;
--color-svgb19-muted: oklch(0.505 0.012 250);
--color-svgb19-win: #ffffff;
--color-svgb19-win: oklch(1 0 0);
--color-svgb19-side: #f2f3f6;
--color-svgb19-side: oklch(0.965 0.004 250);
--color-svgb19-rest: rgba(18, 21, 26, 0.15);
--color-svgb19-rest: oklch(0.19 0.007 250 / 0.15);
--color-svgb19-stroke: #1a63d8;
--color-svgb19-stroke: oklch(0.50 0.20 260);
--font-svgb19-sans: 'Public Sans', system-ui, sans-serif;
--font-svgb19-mono: 'JetBrains Mono', ui-monospace, monospace;
--animate-svgb19-march: svgb19-march 900ms linear infinite;
}
@keyframes svgb19-march {
to {
stroke-dashoffset: -0.1;
}
}
.svgb-19 {
--svgb-19-knob: #ffffff;
}
/* The real fallback is in the markup, as motion-reduce:* utilities — that
variant IS this media query. What cannot be a variant is a *simulated*
setting, so the switch re-applies the identical rules through :has(), which
is what makes the fallback reviewable without changing OS preferences. */
.svgb-19:has(#svgb-19-sim:checked) .svgb-19__ink {
transition: none;
stroke-dasharray: none;
stroke-dashoffset: 0;
opacity: 0;
}
.svgb-19:has(#svgb-19-sim:checked) button:hover .svgb-19__ink,
.svgb-19:has(#svgb-19-sim:checked) button:focus-visible .svgb-19__ink {
opacity: 1;
}
.svgb-19:has(#svgb-19-sim:checked) .svgb-19__ink--march {
animation: none;
stroke-dasharray: 0.05 0.05;
}
.svgb-19:has(#svgb-19-sim:checked) .svgb-19__label {
transition: color 220ms ease;
}
.svgb-19:has(#svgb-19-sim:checked) button:hover .svgb-19__label,
.svgb-19:has(#svgb-19-sim:checked) button:focus-visible .svgb-19__label {
translate: 0 0;
}
.svgb-19:has(#svgb-19-theme:checked) {
--color-svgb19-bg: #0b0d11;
--color-svgb19-bg: oklch(0.165 0.007 250);
--color-svgb19-fg: #edeff3;
--color-svgb19-fg: oklch(0.955 0.004 250);
--color-svgb19-muted: #8c929c;
--color-svgb19-muted: oklch(0.67 0.012 250);
--color-svgb19-win: #15181e;
--color-svgb19-win: oklch(0.22 0.009 250);
--color-svgb19-side: #11141a;
--color-svgb19-side: oklch(0.19 0.008 250);
--color-svgb19-rest: rgba(237, 239, 243, 0.16);
--color-svgb19-rest: oklch(0.955 0.004 250 / 0.16);
--color-svgb19-stroke: #85b4ff;
--color-svgb19-stroke: oklch(0.79 0.13 262);
--svgb-19-knob: #0d1014;
}<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;600&family=JetBrains+Mono:wght@400&display=swap">
<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;600&family=JetBrains+Mono:wght@400&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
- The webfonts load as
<link>tags insetup, not as an@importinside the stylesheet. Tailwind emits user@importrules after its own@layerblocks, and a late@importis invalid CSS — the browser drops it and the demo silently falls back to system fonts. - Tailwind’s
motion-reduce:variant is@media (prefers-reduced-motion: reduce), so the fallback lives in the markup next to the thing it replaces — which is the best argument for utilities in this whole collection: you cannot ship the animation and forget the fallback in another file. - The fallback restates the end state rather than deleting the transition:
motion-reduce:opacity-0plusmotion-reduce:group-hover:opacity-100. A baretransition-nonewould leave a button that never responds at all. motion-reduce:[stroke-dasharray:none]kills the pattern, not just the offset — a zero offset with a live dash pattern can still leave a hairline artefact.- The marching border becomes a static dash with
motion-reduce:animate-none, keeping[stroke-dasharray:0.05_0.05]: the dashed look survives, the travel does not. - The sliding label degrades to
motion-reduce:transition-[color]— colour is not a vestibular channel, translate is. - The simulation switch has to stay in CSS: a variant cannot fake a user preference. It re-applies the same declarations through
:has(), which is why a handful of semantic class names survive as its hooks.
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 Reduced Motion Fallback for Stroke Buttons
Source: https://codefronts.com/components/tailwind-svg-stroke-buttons/tailwind-reduced-motion-fallback-for-stroke-buttons/
The best argument for utilities in the whole collection: Tailwind's motion-reduce: variant is @media (prefers-reduced-motion: reduce), so the fallback lives in the markup next to the thing it replaces — you cannot ship the animation and forget the fallback in another file. And the fallback restates the end state rather than deleting the transition, because a bare transition-none leaves a button that never responds at all.
## HTML
```html
<section class="svgb-19 relative block w-full min-h-svh bg-svgb19-bg text-svgb19-fg font-svgb19-sans">
<input class="peer absolute opacity-0 pointer-events-none" type="checkbox" id="svgb-19-theme">
<label class="absolute z-[5] top-4 right-4 grid place-items-center size-11 rounded-full border border-current/20 bg-current/7 cursor-pointer transition-[background-color,transform] duration-150 hover:bg-current/14 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-19-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(16px,4vw,56px)]">
<div class="w-full max-w-235 grid grid-cols-[minmax(0,200px)_1fr] rounded-[14px] overflow-hidden bg-svgb19-win border border-svgb19-rest shadow-[0_1px_2px_oklch(0_0_0/0.06),0_30px_70px_-50px_oklch(0_0_0/0.4)]">
<div class="flex flex-col gap-0.5 p-2.5 bg-svgb19-side border-e border-svgb19-rest">
<span class="px-2.5 pt-1.5 pb-3 text-xs font-semibold tracking-[0.1em] uppercase text-svgb19-muted">Accessibility</span>
<span class="px-2.5 py-[9px] rounded-[7px] text-sm text-svgb19-muted">Display</span>
<span class="px-2.5 py-[9px] rounded-[7px] text-sm bg-svgb19-stroke/12 text-svgb19-fg font-medium">Motion</span>
<span class="px-2.5 py-[9px] rounded-[7px] text-sm text-svgb19-muted">Pointer</span>
<span class="px-2.5 py-[9px] rounded-[7px] text-sm text-svgb19-muted">Captions</span>
<span class="px-2.5 py-[9px] rounded-[7px] text-sm text-svgb19-muted">Audio</span>
</div>
<div class="flex flex-col gap-4 p-[clamp(20px,3.5vw,34px)]">
<h1 class="m-0 text-[clamp(24px,3.4vw,32px)] font-semibold tracking-[-0.025em]">Motion</h1>
<div class="border border-svgb19-rest rounded-xl">
<input class="peer/sim absolute opacity-0 pointer-events-none" type="checkbox" id="svgb-19-sim">
<label class="flex items-center justify-between gap-5 px-[18px] py-4 cursor-pointer peer-focus-visible/sim:outline-2 peer-focus-visible/sim:outline-svgb19-stroke peer-focus-visible/sim:-outline-offset-2 peer-focus-visible/sim:rounded-xl peer-checked/sim:[&>span:last-child]:bg-svgb19-stroke peer-checked/sim:[&>span:last-child>span]:translate-x-5" for="svgb-19-sim">
<span class="flex flex-col gap-[3px]">
<span class="text-[15px] font-medium">Reduce motion</span>
<span class="text-[13px] text-svgb19-muted">Replaces animated transitions with instant state changes.</span>
</span>
<span class="relative flex-none w-[50px] h-[30px] rounded-full bg-svgb19-fg/16 transition-[background-color] duration-200 motion-reduce:transition-none" aria-hidden="true"><span class="absolute top-[3px] left-[3px] size-6 rounded-full [background:var(--svgb-19-knob)] shadow-[0_1px_3px_oklch(0_0_0/0.3)] transition-[translate] duration-[220ms] ease-[cubic-bezier(0.22,1,0.36,1)] motion-reduce:transition-none"></span></span>
</label>
</div>
<p class="mt-1.5 mb-0 font-svgb19-mono text-[10.5px] tracking-[0.16em] uppercase text-svgb19-muted">Preview</p>
<div class="grid gap-[clamp(12px,2vw,18px)] grid-cols-[repeat(auto-fit,minmax(min(100%,236px),1fr))]">
<div class="flex flex-col gap-[9px]">
<button class="group relative w-full max-w-59 h-15 grid place-items-center border-0 p-0 rounded-xl bg-svgb19-fg/4 text-[14.5px] font-medium cursor-pointer overflow-hidden focus-visible:outline-2 focus-visible:outline-svgb19-stroke focus-visible:outline-offset-[3px]" type="button">
<svg class="absolute inset-0 w-full h-full overflow-visible [&_rect]:fill-none [&_rect]:[stroke-linecap:round]" viewBox="0 0 236 60" aria-hidden="true">
<rect class="stroke-svgb19-rest [stroke-width:1.4]" x="2" y="2" width="232" height="56" rx="12" pathLength="1"/>
<rect class="svgb-19__ink stroke-svgb19-stroke [stroke-width:2.2] [stroke-dasharray:1] [stroke-dashoffset:1] transition-[stroke-dashoffset] duration-[640ms] ease-[cubic-bezier(0.22,1,0.36,1)] group-hover:[stroke-dashoffset:0] group-focus-visible:[stroke-dashoffset:0] 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="2" y="2" width="232" height="56" rx="12" pathLength="1"/>
</svg>
<span class="svgb-19__label relative z-[1] transition-[translate,color] duration-[320ms] ease-[cubic-bezier(0.22,1,0.36,1)]">Perimeter draw</span>
</button>
<span class="font-svgb19-mono text-[10.5px] text-svgb19-muted">travel → instant presence</span>
</div>
<div class="flex flex-col gap-[9px]">
<button class="group relative w-full max-w-59 h-15 grid place-items-center border-0 p-0 rounded-xl bg-svgb19-fg/4 text-[14.5px] font-medium cursor-pointer overflow-hidden focus-visible:outline-2 focus-visible:outline-svgb19-stroke focus-visible:outline-offset-[3px]" type="button">
<svg class="absolute inset-0 w-full h-full overflow-visible [&_rect]:fill-none [&_rect]:[stroke-linecap:round]" viewBox="0 0 236 60" aria-hidden="true">
<rect class="stroke-svgb19-rest [stroke-width:1.4]" x="2" y="2" width="232" height="56" rx="12" pathLength="1"/>
<rect class="svgb-19__ink--march stroke-svgb19-stroke [stroke-width:2.2] [stroke-dasharray:0.05_0.05] [stroke-dashoffset:0] opacity-0 transition-opacity duration-200 animate-svgb19-march [animation-play-state:paused] group-hover:opacity-100 group-hover:[animation-play-state:running] group-focus-visible:opacity-100 group-focus-visible:[animation-play-state:running] motion-reduce:animate-none" x="2" y="2" width="232" height="56" rx="12" pathLength="1"/>
</svg>
<span class="svgb-19__label relative z-[1] transition-[translate,color] duration-[320ms] ease-[cubic-bezier(0.22,1,0.36,1)]">Marching dash</span>
</button>
<span class="font-svgb19-mono text-[10.5px] text-svgb19-muted">loop → static dash</span>
</div>
<div class="flex flex-col gap-[9px]">
<button class="group relative w-full max-w-59 h-15 grid place-items-center border-0 p-0 rounded-xl bg-svgb19-fg/4 text-[14.5px] font-medium cursor-pointer overflow-hidden focus-visible:outline-2 focus-visible:outline-svgb19-stroke focus-visible:outline-offset-[3px]" type="button">
<svg class="absolute inset-0 w-full h-full overflow-visible [&_rect]:fill-none [&_rect]:[stroke-linecap:round]" viewBox="0 0 236 60" aria-hidden="true">
<rect class="stroke-svgb19-rest [stroke-width:1.4]" x="2" y="2" width="232" height="56" rx="12" pathLength="1"/>
<rect class="svgb-19__ink stroke-svgb19-stroke [stroke-width:2.2] [stroke-dasharray:1] [stroke-dashoffset:1] transition-[stroke-dashoffset] duration-[640ms] ease-[cubic-bezier(0.22,1,0.36,1)] group-hover:[stroke-dashoffset:0] group-focus-visible:[stroke-dashoffset:0] 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="2" y="2" width="232" height="56" rx="12" pathLength="1"/>
</svg>
<span class="svgb-19__label relative z-[1] transition-[translate,color] duration-[320ms] ease-[cubic-bezier(0.22,1,0.36,1)] group-hover:-translate-y-0.5 group-hover:text-svgb19-stroke group-focus-visible:-translate-y-0.5 group-focus-visible:text-svgb19-stroke motion-reduce:transition-[color] motion-reduce:group-hover:translate-y-0 motion-reduce:group-focus-visible:translate-y-0">Sliding label</span>
</button>
<span class="font-svgb19-mono text-[10.5px] text-svgb19-muted">slide → colour only</span>
</div>
</div>
<p class="mt-1 mb-0 flex gap-[9px] items-baseline text-[12.5px] text-svgb19-muted"><span class="flex-none font-svgb19-mono text-[10px] px-[7px] py-[3px] rounded-[5px] bg-svgb19-stroke/12 text-[color-mix(in_oklab,var(--color-svgb19-stroke)_78%,var(--color-svgb19-fg))]">WCAG 2.3.3</span> Motion from interaction can be disabled, unless essential.</p>
</div>
</div>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-svgb19-bg: #e7e9ec;
--color-svgb19-bg: oklch(0.935 0.005 250);
--color-svgb19-fg: #12151a;
--color-svgb19-fg: oklch(0.19 0.007 250);
--color-svgb19-muted: #5b616b;
--color-svgb19-muted: oklch(0.505 0.012 250);
--color-svgb19-win: #ffffff;
--color-svgb19-win: oklch(1 0 0);
--color-svgb19-side: #f2f3f6;
--color-svgb19-side: oklch(0.965 0.004 250);
--color-svgb19-rest: rgba(18, 21, 26, 0.15);
--color-svgb19-rest: oklch(0.19 0.007 250 / 0.15);
--color-svgb19-stroke: #1a63d8;
--color-svgb19-stroke: oklch(0.50 0.20 260);
--font-svgb19-sans: 'Public Sans', system-ui, sans-serif;
--font-svgb19-mono: 'JetBrains Mono', ui-monospace, monospace;
--animate-svgb19-march: svgb19-march 900ms linear infinite;
}
@keyframes svgb19-march {
to {
stroke-dashoffset: -0.1;
}
}
.svgb-19 {
--svgb-19-knob: #ffffff;
}
/* The real fallback is in the markup, as motion-reduce:* utilities — that
variant IS this media query. What cannot be a variant is a *simulated*
setting, so the switch re-applies the identical rules through :has(), which
is what makes the fallback reviewable without changing OS preferences. */
.svgb-19:has(#svgb-19-sim:checked) .svgb-19__ink {
transition: none;
stroke-dasharray: none;
stroke-dashoffset: 0;
opacity: 0;
}
.svgb-19:has(#svgb-19-sim:checked) button:hover .svgb-19__ink,
.svgb-19:has(#svgb-19-sim:checked) button:focus-visible .svgb-19__ink {
opacity: 1;
}
.svgb-19:has(#svgb-19-sim:checked) .svgb-19__ink--march {
animation: none;
stroke-dasharray: 0.05 0.05;
}
.svgb-19:has(#svgb-19-sim:checked) .svgb-19__label {
transition: color 220ms ease;
}
.svgb-19:has(#svgb-19-sim:checked) button:hover .svgb-19__label,
.svgb-19:has(#svgb-19-sim:checked) button:focus-visible .svgb-19__label {
translate: 0 0;
}
.svgb-19:has(#svgb-19-theme:checked) {
--color-svgb19-bg: #0b0d11;
--color-svgb19-bg: oklch(0.165 0.007 250);
--color-svgb19-fg: #edeff3;
--color-svgb19-fg: oklch(0.955 0.004 250);
--color-svgb19-muted: #8c929c;
--color-svgb19-muted: oklch(0.67 0.012 250);
--color-svgb19-win: #15181e;
--color-svgb19-win: oklch(0.22 0.009 250);
--color-svgb19-side: #11141a;
--color-svgb19-side: oklch(0.19 0.008 250);
--color-svgb19-rest: rgba(237, 239, 243, 0.16);
--color-svgb19-rest: oklch(0.955 0.004 250 / 0.16);
--color-svgb19-stroke: #85b4ff;
--color-svgb19-stroke: oklch(0.79 0.13 262);
--svgb-19-knob: #0d1014;
}
```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 Reduced Motion Fallback for Stroke Buttons
Source: https://codefronts.com/components/tailwind-svg-stroke-buttons/tailwind-reduced-motion-fallback-for-stroke-buttons/
The best argument for utilities in the whole collection: Tailwind's motion-reduce: variant is @media (prefers-reduced-motion: reduce), so the fallback lives in the markup next to the thing it replaces — you cannot ship the animation and forget the fallback in another file. And the fallback restates the end state rather than deleting the transition, because a bare transition-none leaves a button that never responds at all.
## HTML
```html
<section class="svgb-19 relative block w-full min-h-svh bg-svgb19-bg text-svgb19-fg font-svgb19-sans">
<input class="peer absolute opacity-0 pointer-events-none" type="checkbox" id="svgb-19-theme">
<label class="absolute z-[5] top-4 right-4 grid place-items-center size-11 rounded-full border border-current/20 bg-current/7 cursor-pointer transition-[background-color,transform] duration-150 hover:bg-current/14 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-19-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(16px,4vw,56px)]">
<div class="w-full max-w-235 grid grid-cols-[minmax(0,200px)_1fr] rounded-[14px] overflow-hidden bg-svgb19-win border border-svgb19-rest shadow-[0_1px_2px_oklch(0_0_0/0.06),0_30px_70px_-50px_oklch(0_0_0/0.4)]">
<div class="flex flex-col gap-0.5 p-2.5 bg-svgb19-side border-e border-svgb19-rest">
<span class="px-2.5 pt-1.5 pb-3 text-xs font-semibold tracking-[0.1em] uppercase text-svgb19-muted">Accessibility</span>
<span class="px-2.5 py-[9px] rounded-[7px] text-sm text-svgb19-muted">Display</span>
<span class="px-2.5 py-[9px] rounded-[7px] text-sm bg-svgb19-stroke/12 text-svgb19-fg font-medium">Motion</span>
<span class="px-2.5 py-[9px] rounded-[7px] text-sm text-svgb19-muted">Pointer</span>
<span class="px-2.5 py-[9px] rounded-[7px] text-sm text-svgb19-muted">Captions</span>
<span class="px-2.5 py-[9px] rounded-[7px] text-sm text-svgb19-muted">Audio</span>
</div>
<div class="flex flex-col gap-4 p-[clamp(20px,3.5vw,34px)]">
<h1 class="m-0 text-[clamp(24px,3.4vw,32px)] font-semibold tracking-[-0.025em]">Motion</h1>
<div class="border border-svgb19-rest rounded-xl">
<input class="peer/sim absolute opacity-0 pointer-events-none" type="checkbox" id="svgb-19-sim">
<label class="flex items-center justify-between gap-5 px-[18px] py-4 cursor-pointer peer-focus-visible/sim:outline-2 peer-focus-visible/sim:outline-svgb19-stroke peer-focus-visible/sim:-outline-offset-2 peer-focus-visible/sim:rounded-xl peer-checked/sim:[&>span:last-child]:bg-svgb19-stroke peer-checked/sim:[&>span:last-child>span]:translate-x-5" for="svgb-19-sim">
<span class="flex flex-col gap-[3px]">
<span class="text-[15px] font-medium">Reduce motion</span>
<span class="text-[13px] text-svgb19-muted">Replaces animated transitions with instant state changes.</span>
</span>
<span class="relative flex-none w-[50px] h-[30px] rounded-full bg-svgb19-fg/16 transition-[background-color] duration-200 motion-reduce:transition-none" aria-hidden="true"><span class="absolute top-[3px] left-[3px] size-6 rounded-full [background:var(--svgb-19-knob)] shadow-[0_1px_3px_oklch(0_0_0/0.3)] transition-[translate] duration-[220ms] ease-[cubic-bezier(0.22,1,0.36,1)] motion-reduce:transition-none"></span></span>
</label>
</div>
<p class="mt-1.5 mb-0 font-svgb19-mono text-[10.5px] tracking-[0.16em] uppercase text-svgb19-muted">Preview</p>
<div class="grid gap-[clamp(12px,2vw,18px)] grid-cols-[repeat(auto-fit,minmax(min(100%,236px),1fr))]">
<div class="flex flex-col gap-[9px]">
<button class="group relative w-full max-w-59 h-15 grid place-items-center border-0 p-0 rounded-xl bg-svgb19-fg/4 text-[14.5px] font-medium cursor-pointer overflow-hidden focus-visible:outline-2 focus-visible:outline-svgb19-stroke focus-visible:outline-offset-[3px]" type="button">
<svg class="absolute inset-0 w-full h-full overflow-visible [&_rect]:fill-none [&_rect]:[stroke-linecap:round]" viewBox="0 0 236 60" aria-hidden="true">
<rect class="stroke-svgb19-rest [stroke-width:1.4]" x="2" y="2" width="232" height="56" rx="12" pathLength="1"/>
<rect class="svgb-19__ink stroke-svgb19-stroke [stroke-width:2.2] [stroke-dasharray:1] [stroke-dashoffset:1] transition-[stroke-dashoffset] duration-[640ms] ease-[cubic-bezier(0.22,1,0.36,1)] group-hover:[stroke-dashoffset:0] group-focus-visible:[stroke-dashoffset:0] 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="2" y="2" width="232" height="56" rx="12" pathLength="1"/>
</svg>
<span class="svgb-19__label relative z-[1] transition-[translate,color] duration-[320ms] ease-[cubic-bezier(0.22,1,0.36,1)]">Perimeter draw</span>
</button>
<span class="font-svgb19-mono text-[10.5px] text-svgb19-muted">travel → instant presence</span>
</div>
<div class="flex flex-col gap-[9px]">
<button class="group relative w-full max-w-59 h-15 grid place-items-center border-0 p-0 rounded-xl bg-svgb19-fg/4 text-[14.5px] font-medium cursor-pointer overflow-hidden focus-visible:outline-2 focus-visible:outline-svgb19-stroke focus-visible:outline-offset-[3px]" type="button">
<svg class="absolute inset-0 w-full h-full overflow-visible [&_rect]:fill-none [&_rect]:[stroke-linecap:round]" viewBox="0 0 236 60" aria-hidden="true">
<rect class="stroke-svgb19-rest [stroke-width:1.4]" x="2" y="2" width="232" height="56" rx="12" pathLength="1"/>
<rect class="svgb-19__ink--march stroke-svgb19-stroke [stroke-width:2.2] [stroke-dasharray:0.05_0.05] [stroke-dashoffset:0] opacity-0 transition-opacity duration-200 animate-svgb19-march [animation-play-state:paused] group-hover:opacity-100 group-hover:[animation-play-state:running] group-focus-visible:opacity-100 group-focus-visible:[animation-play-state:running] motion-reduce:animate-none" x="2" y="2" width="232" height="56" rx="12" pathLength="1"/>
</svg>
<span class="svgb-19__label relative z-[1] transition-[translate,color] duration-[320ms] ease-[cubic-bezier(0.22,1,0.36,1)]">Marching dash</span>
</button>
<span class="font-svgb19-mono text-[10.5px] text-svgb19-muted">loop → static dash</span>
</div>
<div class="flex flex-col gap-[9px]">
<button class="group relative w-full max-w-59 h-15 grid place-items-center border-0 p-0 rounded-xl bg-svgb19-fg/4 text-[14.5px] font-medium cursor-pointer overflow-hidden focus-visible:outline-2 focus-visible:outline-svgb19-stroke focus-visible:outline-offset-[3px]" type="button">
<svg class="absolute inset-0 w-full h-full overflow-visible [&_rect]:fill-none [&_rect]:[stroke-linecap:round]" viewBox="0 0 236 60" aria-hidden="true">
<rect class="stroke-svgb19-rest [stroke-width:1.4]" x="2" y="2" width="232" height="56" rx="12" pathLength="1"/>
<rect class="svgb-19__ink stroke-svgb19-stroke [stroke-width:2.2] [stroke-dasharray:1] [stroke-dashoffset:1] transition-[stroke-dashoffset] duration-[640ms] ease-[cubic-bezier(0.22,1,0.36,1)] group-hover:[stroke-dashoffset:0] group-focus-visible:[stroke-dashoffset:0] 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="2" y="2" width="232" height="56" rx="12" pathLength="1"/>
</svg>
<span class="svgb-19__label relative z-[1] transition-[translate,color] duration-[320ms] ease-[cubic-bezier(0.22,1,0.36,1)] group-hover:-translate-y-0.5 group-hover:text-svgb19-stroke group-focus-visible:-translate-y-0.5 group-focus-visible:text-svgb19-stroke motion-reduce:transition-[color] motion-reduce:group-hover:translate-y-0 motion-reduce:group-focus-visible:translate-y-0">Sliding label</span>
</button>
<span class="font-svgb19-mono text-[10.5px] text-svgb19-muted">slide → colour only</span>
</div>
</div>
<p class="mt-1 mb-0 flex gap-[9px] items-baseline text-[12.5px] text-svgb19-muted"><span class="flex-none font-svgb19-mono text-[10px] px-[7px] py-[3px] rounded-[5px] bg-svgb19-stroke/12 text-[color-mix(in_oklab,var(--color-svgb19-stroke)_78%,var(--color-svgb19-fg))]">WCAG 2.3.3</span> Motion from interaction can be disabled, unless essential.</p>
</div>
</div>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-svgb19-bg: #e7e9ec;
--color-svgb19-bg: oklch(0.935 0.005 250);
--color-svgb19-fg: #12151a;
--color-svgb19-fg: oklch(0.19 0.007 250);
--color-svgb19-muted: #5b616b;
--color-svgb19-muted: oklch(0.505 0.012 250);
--color-svgb19-win: #ffffff;
--color-svgb19-win: oklch(1 0 0);
--color-svgb19-side: #f2f3f6;
--color-svgb19-side: oklch(0.965 0.004 250);
--color-svgb19-rest: rgba(18, 21, 26, 0.15);
--color-svgb19-rest: oklch(0.19 0.007 250 / 0.15);
--color-svgb19-stroke: #1a63d8;
--color-svgb19-stroke: oklch(0.50 0.20 260);
--font-svgb19-sans: 'Public Sans', system-ui, sans-serif;
--font-svgb19-mono: 'JetBrains Mono', ui-monospace, monospace;
--animate-svgb19-march: svgb19-march 900ms linear infinite;
}
@keyframes svgb19-march {
to {
stroke-dashoffset: -0.1;
}
}
.svgb-19 {
--svgb-19-knob: #ffffff;
}
/* The real fallback is in the markup, as motion-reduce:* utilities — that
variant IS this media query. What cannot be a variant is a *simulated*
setting, so the switch re-applies the identical rules through :has(), which
is what makes the fallback reviewable without changing OS preferences. */
.svgb-19:has(#svgb-19-sim:checked) .svgb-19__ink {
transition: none;
stroke-dasharray: none;
stroke-dashoffset: 0;
opacity: 0;
}
.svgb-19:has(#svgb-19-sim:checked) button:hover .svgb-19__ink,
.svgb-19:has(#svgb-19-sim:checked) button:focus-visible .svgb-19__ink {
opacity: 1;
}
.svgb-19:has(#svgb-19-sim:checked) .svgb-19__ink--march {
animation: none;
stroke-dasharray: 0.05 0.05;
}
.svgb-19:has(#svgb-19-sim:checked) .svgb-19__label {
transition: color 220ms ease;
}
.svgb-19:has(#svgb-19-sim:checked) button:hover .svgb-19__label,
.svgb-19:has(#svgb-19-sim:checked) button:focus-visible .svgb-19__label {
translate: 0 0;
}
.svgb-19:has(#svgb-19-theme:checked) {
--color-svgb19-bg: #0b0d11;
--color-svgb19-bg: oklch(0.165 0.007 250);
--color-svgb19-fg: #edeff3;
--color-svgb19-fg: oklch(0.955 0.004 250);
--color-svgb19-muted: #8c929c;
--color-svgb19-muted: oklch(0.67 0.012 250);
--color-svgb19-win: #15181e;
--color-svgb19-win: oklch(0.22 0.009 250);
--color-svgb19-side: #11141a;
--color-svgb19-side: oklch(0.19 0.008 250);
--color-svgb19-rest: rgba(237, 239, 243, 0.16);
--color-svgb19-rest: oklch(0.955 0.004 250 / 0.16);
--color-svgb19-stroke: #85b4ff;
--color-svgb19-stroke: oklch(0.79 0.13 262);
--svgb-19-knob: #0d1014;
}
```How this works
The variant is the media query. There is no indirection to get wrong:
class="[stroke-dasharray:1] [stroke-dashoffset:1]
transition-[stroke-dashoffset] duration-[600ms]
group-hover:[stroke-dashoffset:0]
motion-reduce:transition-none
motion-reduce:[stroke-dasharray:none]
motion-reduce:[stroke-dashoffset:0]
motion-reduce:opacity-0
motion-reduce:group-hover:opacity-100"Read that as two designs on one element: an animated one and a static one. Neither can be shipped without the other being visible in the same string.
Replace the motion; do not remove the feedback. The naive fallback is motion-reduce:transition-none alone, which leaves a button whose border never changes — the animation was the only hover signal, and now there is none. The fix is to restate the end state and move the reveal to opacity, which is instant and not vestibular.
Kill the pattern, not just the offset. [stroke-dashoffset:0] with a live dash array can still leave a sliver or a seam depending on the path. motion-reduce:[stroke-dasharray:none] makes the line a plain continuous stroke.
Not all properties are equal. The sliding label degrades to motion-reduce:transition-[color]: the colour change survives, the translate does not. Reduced motion is about vestibular triggers, not about removing all feedback — colour, opacity and border weight are all still available to you.
Why the switch is CSS. A variant cannot fake a user preference, so the on-page simulation re-applies the same declarations through a :has(:checked) block. That is also why a handful of semantic class names survive here as hooks — the demo has to be able to demonstrate itself.
Make it yours
- Copy the
motion-reduce:block whenever you copy a stroke animation. That is the habit this demo exists to build. - Use
motion-safe:instead if you prefer opting in to motion — same idea, inverted default. - Keep the reduced-motion reveal instant. A slower animation is not a reduced-motion fallback.
- Adjust the static border weight if the instant state feels weak next to the animated one; matching perceived strength is the goal.
- Leave the simulation switch out of production — it is a teaching device, and the real query is what ships.
Gotchas — read before shipping
motion-reduce:transition-noneon its own is the most common mistake in the whole subject: it removes the animation and the affordance together.- Reduced motion does not mean no feedback. Colour, opacity and weight are all safe channels.
- A zero dash offset with a live dash pattern can still render a seam. Clear the pattern too.
- Media-query variants do not raise specificity, so a
motion-reduce:utility can lose to agroup-hover:one in the same layer — that is when you need!. - A simulated toggle is not the real query. Test with the OS setting on before you believe it.
- Do not swap a travelling stroke for a slower travelling stroke. The requirement is less motion, not slower motion.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
prefers-reduced-motion is supported in every current engine and has been for years. The floor is Tailwind v4's generated sheet; the on-page simulation additionally needs :has(), which the real media query does not.