08 / 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 Stroke Draw Button That Reverses on Mouse Leave
Three keycaps, three transition declarations, side by side in the markup where you can actually compare them. The symmetric key reverses from wherever the pointer left it; the broken key uses group-hover:animate-svgb08-draw and snaps, because an animation has no memory of an interrupted position; the third declares a full [transition:...] twice to get 300ms in and 760ms out.
Nucleo: 44,000+ SVG icons in five families · All Icons Bundle $149
Nucleo · Affiliate link
The code
<section class="svgb-08 relative block w-full min-h-svh bg-svgb08-bg text-svgb08-fg font-svgb08-sans">
<input class="peer absolute opacity-0 pointer-events-none" type="checkbox" id="svgb-08-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/15 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-08-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-content-center justify-items-center gap-[clamp(22px,4vh,40px)] p-[clamp(20px,5vw,70px)]">
<div class="flex flex-wrap justify-center gap-[clamp(18px,3vw,34px)]">
<div class="flex flex-col items-center gap-3.5">
<button class="group relative size-33 grid place-items-center border-0 p-0 rounded-[18px] [background:linear-gradient(180deg,color-mix(in_oklab,var(--color-svgb08-cap)_88%,#fff_12%),var(--color-svgb08-cap))] [box-shadow:var(--svgb-08-key-sh)] transition-[translate,box-shadow] duration-100 motion-reduce:transition-none active:translate-y-[5px] active:[box-shadow:var(--svgb-08-key-sh-a)] focus-visible:outline-none focus-visible:[box-shadow:var(--svgb-08-key-sh-f)]" type="button">
<svg class="absolute inset-0 w-full h-full overflow-visible" viewBox="0 0 132 132" aria-hidden="true">
<rect class="fill-none stroke-svgb08-rest [stroke-width:1.4] [stroke-linecap:round]" x="2" y="2" width="128" height="128" rx="18" vector-effect="non-scaling-stroke"/>
<rect class="fill-none [stroke-width:2.4] [stroke-linecap:round] [stroke-dasharray:490] [stroke-dashoffset:490] stroke-svgb08-stroke transition-[stroke-dashoffset] duration-[520ms] ease-[cubic-bezier(0.45,0,0.55,1)] group-hover:[stroke-dashoffset:0] group-focus-visible:[stroke-dashoffset:0] motion-reduce:!transition-none motion-reduce:!animate-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="128" height="128" rx="18" vector-effect="non-scaling-stroke"/>
</svg>
<span class="relative z-[1] flex flex-col items-center gap-2">
<span class="text-3xl leading-none transition-colors duration-300 motion-reduce:transition-none group-hover:text-svgb08-stroke group-focus-visible:text-svgb08-stroke">↶</span>
<span class="font-svgb08-mono text-[10.5px] tracking-[0.16em] uppercase text-svgb08-muted">reverse</span>
</span>
</button>
<span class="inline-flex items-center gap-[7px] font-svgb08-mono text-[11px] text-[color-mix(in_oklab,var(--color-svgb08-stroke)_62%,var(--color-svgb08-muted))] before:content-[''] before:size-1.5 before:rounded-full before:bg-current">transition · 520 / 520</span>
</div>
<div class="flex flex-col items-center gap-3.5">
<button class="group relative size-33 grid place-items-center border-0 p-0 rounded-[18px] [background:linear-gradient(180deg,color-mix(in_oklab,var(--color-svgb08-cap)_88%,#fff_12%),var(--color-svgb08-cap))] [box-shadow:var(--svgb-08-key-sh)] transition-[translate,box-shadow] duration-100 motion-reduce:transition-none active:translate-y-[5px] active:[box-shadow:var(--svgb-08-key-sh-a)] focus-visible:outline-none focus-visible:[box-shadow:var(--svgb-08-key-sh-f)]" type="button">
<svg class="absolute inset-0 w-full h-full overflow-visible" viewBox="0 0 132 132" aria-hidden="true">
<rect class="fill-none stroke-svgb08-rest [stroke-width:1.4] [stroke-linecap:round]" x="2" y="2" width="128" height="128" rx="18" vector-effect="non-scaling-stroke"/>
<rect class="fill-none [stroke-width:2.4] [stroke-linecap:round] [stroke-dasharray:490] [stroke-dashoffset:490] stroke-svgb08-warn group-hover:animate-svgb08-draw group-focus-visible:animate-svgb08-draw motion-reduce:!transition-none motion-reduce:!animate-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="128" height="128" rx="18" vector-effect="non-scaling-stroke"/>
</svg>
<span class="relative z-[1] flex flex-col items-center gap-2">
<span class="text-3xl leading-none transition-colors duration-300 motion-reduce:transition-none group-hover:text-svgb08-warn group-focus-visible:text-svgb08-warn">□</span>
<span class="font-svgb08-mono text-[10.5px] tracking-[0.16em] uppercase text-svgb08-muted">snap</span>
</span>
</button>
<span class="inline-flex items-center gap-[7px] font-svgb08-mono text-[11px] text-[color-mix(in_oklab,var(--color-svgb08-warn)_62%,var(--color-svgb08-muted))] before:content-[''] before:size-1.5 before:rounded-full before:bg-current">keyframes · no memory</span>
</div>
<div class="flex flex-col items-center gap-3.5">
<button class="group relative size-33 grid place-items-center border-0 p-0 rounded-[18px] [background:linear-gradient(180deg,color-mix(in_oklab,var(--color-svgb08-cap)_88%,#fff_12%),var(--color-svgb08-cap))] [box-shadow:var(--svgb-08-key-sh)] transition-[translate,box-shadow] duration-100 motion-reduce:transition-none active:translate-y-[5px] active:[box-shadow:var(--svgb-08-key-sh-a)] focus-visible:outline-none focus-visible:[box-shadow:var(--svgb-08-key-sh-f)]" type="button">
<svg class="absolute inset-0 w-full h-full overflow-visible" viewBox="0 0 132 132" aria-hidden="true">
<rect class="fill-none stroke-svgb08-rest [stroke-width:1.4] [stroke-linecap:round]" x="2" y="2" width="128" height="128" rx="18" vector-effect="non-scaling-stroke"/>
<rect class="fill-none [stroke-width:2.4] [stroke-linecap:round] [stroke-dasharray:490] [stroke-dashoffset:490] stroke-svgb08-stroke [transition:stroke-dashoffset_760ms_cubic-bezier(0.22,1,0.36,1)_40ms] group-hover:[stroke-dashoffset:0] group-hover:[transition:stroke-dashoffset_300ms_cubic-bezier(0.4,0,0.2,1)_0ms] group-focus-visible:[stroke-dashoffset:0] group-focus-visible:[transition:stroke-dashoffset_300ms_cubic-bezier(0.4,0,0.2,1)_0ms] motion-reduce:!transition-none motion-reduce:!animate-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="128" height="128" rx="18" vector-effect="non-scaling-stroke"/>
</svg>
<span class="relative z-[1] flex flex-col items-center gap-2">
<span class="text-3xl leading-none transition-colors duration-300 motion-reduce:transition-none group-hover:text-svgb08-stroke group-focus-visible:text-svgb08-stroke">⇥</span>
<span class="font-svgb08-mono text-[10.5px] tracking-[0.16em] uppercase text-svgb08-muted">asym</span>
</span>
</button>
<span class="inline-flex items-center gap-[7px] font-svgb08-mono text-[11px] text-[color-mix(in_oklab,var(--color-svgb08-stroke)_62%,var(--color-svgb08-muted))] before:content-[''] before:size-1.5 before:rounded-full before:bg-current">300 in · 760 out</span>
</div>
</div>
<div class="px-[18px] py-3 rounded-[10px] bg-svgb08-fg/4 border border-svgb08-rest">
<span class="font-svgb08-mono text-[11px] text-svgb08-muted">interrupt a draw halfway — only a transition can walk it back</span>
</div>
</div>
</section><section class="svgb-08 relative block w-full min-h-svh bg-svgb08-bg text-svgb08-fg font-svgb08-sans">
<input class="peer absolute opacity-0 pointer-events-none" type="checkbox" id="svgb-08-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/15 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-08-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-content-center justify-items-center gap-[clamp(22px,4vh,40px)] p-[clamp(20px,5vw,70px)]">
<div class="flex flex-wrap justify-center gap-[clamp(18px,3vw,34px)]">
<div class="flex flex-col items-center gap-3.5">
<button class="group relative size-33 grid place-items-center border-0 p-0 rounded-[18px] [background:linear-gradient(180deg,color-mix(in_oklab,var(--color-svgb08-cap)_88%,#fff_12%),var(--color-svgb08-cap))] [box-shadow:var(--svgb-08-key-sh)] transition-[translate,box-shadow] duration-100 motion-reduce:transition-none active:translate-y-[5px] active:[box-shadow:var(--svgb-08-key-sh-a)] focus-visible:outline-none focus-visible:[box-shadow:var(--svgb-08-key-sh-f)]" type="button">
<svg class="absolute inset-0 w-full h-full overflow-visible" viewBox="0 0 132 132" aria-hidden="true">
<rect class="fill-none stroke-svgb08-rest [stroke-width:1.4] [stroke-linecap:round]" x="2" y="2" width="128" height="128" rx="18" vector-effect="non-scaling-stroke"/>
<rect class="fill-none [stroke-width:2.4] [stroke-linecap:round] [stroke-dasharray:490] [stroke-dashoffset:490] stroke-svgb08-stroke transition-[stroke-dashoffset] duration-[520ms] ease-[cubic-bezier(0.45,0,0.55,1)] group-hover:[stroke-dashoffset:0] group-focus-visible:[stroke-dashoffset:0] motion-reduce:!transition-none motion-reduce:!animate-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="128" height="128" rx="18" vector-effect="non-scaling-stroke"/>
</svg>
<span class="relative z-[1] flex flex-col items-center gap-2">
<span class="text-3xl leading-none transition-colors duration-300 motion-reduce:transition-none group-hover:text-svgb08-stroke group-focus-visible:text-svgb08-stroke">↶</span>
<span class="font-svgb08-mono text-[10.5px] tracking-[0.16em] uppercase text-svgb08-muted">reverse</span>
</span>
</button>
<span class="inline-flex items-center gap-[7px] font-svgb08-mono text-[11px] text-[color-mix(in_oklab,var(--color-svgb08-stroke)_62%,var(--color-svgb08-muted))] before:content-[''] before:size-1.5 before:rounded-full before:bg-current">transition · 520 / 520</span>
</div>
<div class="flex flex-col items-center gap-3.5">
<button class="group relative size-33 grid place-items-center border-0 p-0 rounded-[18px] [background:linear-gradient(180deg,color-mix(in_oklab,var(--color-svgb08-cap)_88%,#fff_12%),var(--color-svgb08-cap))] [box-shadow:var(--svgb-08-key-sh)] transition-[translate,box-shadow] duration-100 motion-reduce:transition-none active:translate-y-[5px] active:[box-shadow:var(--svgb-08-key-sh-a)] focus-visible:outline-none focus-visible:[box-shadow:var(--svgb-08-key-sh-f)]" type="button">
<svg class="absolute inset-0 w-full h-full overflow-visible" viewBox="0 0 132 132" aria-hidden="true">
<rect class="fill-none stroke-svgb08-rest [stroke-width:1.4] [stroke-linecap:round]" x="2" y="2" width="128" height="128" rx="18" vector-effect="non-scaling-stroke"/>
<rect class="fill-none [stroke-width:2.4] [stroke-linecap:round] [stroke-dasharray:490] [stroke-dashoffset:490] stroke-svgb08-warn group-hover:animate-svgb08-draw group-focus-visible:animate-svgb08-draw motion-reduce:!transition-none motion-reduce:!animate-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="128" height="128" rx="18" vector-effect="non-scaling-stroke"/>
</svg>
<span class="relative z-[1] flex flex-col items-center gap-2">
<span class="text-3xl leading-none transition-colors duration-300 motion-reduce:transition-none group-hover:text-svgb08-warn group-focus-visible:text-svgb08-warn">□</span>
<span class="font-svgb08-mono text-[10.5px] tracking-[0.16em] uppercase text-svgb08-muted">snap</span>
</span>
</button>
<span class="inline-flex items-center gap-[7px] font-svgb08-mono text-[11px] text-[color-mix(in_oklab,var(--color-svgb08-warn)_62%,var(--color-svgb08-muted))] before:content-[''] before:size-1.5 before:rounded-full before:bg-current">keyframes · no memory</span>
</div>
<div class="flex flex-col items-center gap-3.5">
<button class="group relative size-33 grid place-items-center border-0 p-0 rounded-[18px] [background:linear-gradient(180deg,color-mix(in_oklab,var(--color-svgb08-cap)_88%,#fff_12%),var(--color-svgb08-cap))] [box-shadow:var(--svgb-08-key-sh)] transition-[translate,box-shadow] duration-100 motion-reduce:transition-none active:translate-y-[5px] active:[box-shadow:var(--svgb-08-key-sh-a)] focus-visible:outline-none focus-visible:[box-shadow:var(--svgb-08-key-sh-f)]" type="button">
<svg class="absolute inset-0 w-full h-full overflow-visible" viewBox="0 0 132 132" aria-hidden="true">
<rect class="fill-none stroke-svgb08-rest [stroke-width:1.4] [stroke-linecap:round]" x="2" y="2" width="128" height="128" rx="18" vector-effect="non-scaling-stroke"/>
<rect class="fill-none [stroke-width:2.4] [stroke-linecap:round] [stroke-dasharray:490] [stroke-dashoffset:490] stroke-svgb08-stroke [transition:stroke-dashoffset_760ms_cubic-bezier(0.22,1,0.36,1)_40ms] group-hover:[stroke-dashoffset:0] group-hover:[transition:stroke-dashoffset_300ms_cubic-bezier(0.4,0,0.2,1)_0ms] group-focus-visible:[stroke-dashoffset:0] group-focus-visible:[transition:stroke-dashoffset_300ms_cubic-bezier(0.4,0,0.2,1)_0ms] motion-reduce:!transition-none motion-reduce:!animate-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="128" height="128" rx="18" vector-effect="non-scaling-stroke"/>
</svg>
<span class="relative z-[1] flex flex-col items-center gap-2">
<span class="text-3xl leading-none transition-colors duration-300 motion-reduce:transition-none group-hover:text-svgb08-stroke group-focus-visible:text-svgb08-stroke">⇥</span>
<span class="font-svgb08-mono text-[10.5px] tracking-[0.16em] uppercase text-svgb08-muted">asym</span>
</span>
</button>
<span class="inline-flex items-center gap-[7px] font-svgb08-mono text-[11px] text-[color-mix(in_oklab,var(--color-svgb08-stroke)_62%,var(--color-svgb08-muted))] before:content-[''] before:size-1.5 before:rounded-full before:bg-current">300 in · 760 out</span>
</div>
</div>
<div class="px-[18px] py-3 rounded-[10px] bg-svgb08-fg/4 border border-svgb08-rest">
<span class="font-svgb08-mono text-[11px] text-svgb08-muted">interrupt a draw halfway — only a transition can walk it back</span>
</div>
</div>
</section>@import "tailwindcss";
@theme {
--color-svgb08-bg: #17181b;
--color-svgb08-bg: oklch(0.225 0.004 250);
--color-svgb08-cap: #23252a;
--color-svgb08-cap: oklch(0.275 0.006 255);
--color-svgb08-fg: #e9ebee;
--color-svgb08-fg: oklch(0.94 0.004 250);
--color-svgb08-muted: #868b93;
--color-svgb08-muted: oklch(0.655 0.011 250);
--color-svgb08-rest: rgba(233, 235, 238, 0.14);
--color-svgb08-rest: oklch(0.94 0.004 250 / 0.14);
--color-svgb08-stroke: #c6ff4f;
--color-svgb08-stroke: oklch(0.92 0.20 122);
--color-svgb08-warn: #ff7a6b;
--color-svgb08-warn: oklch(0.75 0.16 30);
--font-svgb08-sans: 'Figtree', system-ui, sans-serif;
--font-svgb08-mono: 'JetBrains Mono', ui-monospace, monospace;
/* the wrong way, kept on purpose: a keyframe cannot resume from an
interrupted position, so this key snaps back on mouse-out */
--animate-svgb08-draw: svgb-08-draw 520ms cubic-bezier(0.45, 0, 0.55, 1) forwards;
}
@keyframes svgb-08-draw {
from {
stroke-dashoffset: 490;
}
to {
stroke-dashoffset: 0;
}
}
/* Four-layer keycap shadows, one set per interaction state. Kept as variables
so the markup reads [box-shadow:var(--svgb-08-key-sh)] rather than three
200-character arbitrary values, and so the light scheme swaps all of them. */
.svgb-08 {
--svgb-08-key-sh: inset 0 1px 0 oklch(1 0 0 / 0.09), inset 0 -3px 0 oklch(0 0 0 / 0.35), 0 8px 0 color-mix(in oklab, var(--color-svgb08-cap) 70%, #000), 0 18px 30px -14px oklch(0 0 0 / 0.7);
--svgb-08-key-sh-a: inset 0 1px 0 oklch(1 0 0 / 0.07), inset 0 -2px 0 oklch(0 0 0 / 0.3), 0 3px 0 color-mix(in oklab, var(--color-svgb08-cap) 70%, #000), 0 8px 16px -10px oklch(0 0 0 / 0.7);
--svgb-08-key-sh-f: 0 0 0 3px color-mix(in oklab, var(--color-svgb08-stroke) 40%, transparent), 0 8px 0 color-mix(in oklab, var(--color-svgb08-cap) 70%, #000);
}
.svgb-08:has(#svgb-08-theme:checked) {
--color-svgb08-bg: #e8e9ec;
--color-svgb08-bg: oklch(0.935 0.004 250);
--color-svgb08-cap: #fbfbfc;
--color-svgb08-cap: oklch(0.985 0.002 250);
--color-svgb08-fg: #14171b;
--color-svgb08-fg: oklch(0.20 0.005 250);
--color-svgb08-muted: #5a6068;
--color-svgb08-muted: oklch(0.50 0.011 250);
--color-svgb08-rest: rgba(20, 23, 27, 0.16);
--color-svgb08-rest: oklch(0.20 0.005 250 / 0.16);
--color-svgb08-stroke: #3f6b00;
--color-svgb08-stroke: oklch(0.48 0.16 126);
--color-svgb08-warn: #b03a26;
--color-svgb08-warn: oklch(0.50 0.17 30);
--svgb-08-key-sh: inset 0 1px 0 oklch(1 0 0 / 0.9), inset 0 -3px 0 oklch(0 0 0 / 0.08), 0 8px 0 color-mix(in oklab, var(--color-svgb08-cap) 78%, #000 22%), 0 18px 30px -18px oklch(0 0 0 / 0.4);
}@import "tailwindcss";
@theme {
--color-svgb08-bg: #17181b;
--color-svgb08-bg: oklch(0.225 0.004 250);
--color-svgb08-cap: #23252a;
--color-svgb08-cap: oklch(0.275 0.006 255);
--color-svgb08-fg: #e9ebee;
--color-svgb08-fg: oklch(0.94 0.004 250);
--color-svgb08-muted: #868b93;
--color-svgb08-muted: oklch(0.655 0.011 250);
--color-svgb08-rest: rgba(233, 235, 238, 0.14);
--color-svgb08-rest: oklch(0.94 0.004 250 / 0.14);
--color-svgb08-stroke: #c6ff4f;
--color-svgb08-stroke: oklch(0.92 0.20 122);
--color-svgb08-warn: #ff7a6b;
--color-svgb08-warn: oklch(0.75 0.16 30);
--font-svgb08-sans: 'Figtree', system-ui, sans-serif;
--font-svgb08-mono: 'JetBrains Mono', ui-monospace, monospace;
/* the wrong way, kept on purpose: a keyframe cannot resume from an
interrupted position, so this key snaps back on mouse-out */
--animate-svgb08-draw: svgb-08-draw 520ms cubic-bezier(0.45, 0, 0.55, 1) forwards;
}
@keyframes svgb-08-draw {
from {
stroke-dashoffset: 490;
}
to {
stroke-dashoffset: 0;
}
}
/* Four-layer keycap shadows, one set per interaction state. Kept as variables
so the markup reads [box-shadow:var(--svgb-08-key-sh)] rather than three
200-character arbitrary values, and so the light scheme swaps all of them. */
.svgb-08 {
--svgb-08-key-sh: inset 0 1px 0 oklch(1 0 0 / 0.09), inset 0 -3px 0 oklch(0 0 0 / 0.35), 0 8px 0 color-mix(in oklab, var(--color-svgb08-cap) 70%, #000), 0 18px 30px -14px oklch(0 0 0 / 0.7);
--svgb-08-key-sh-a: inset 0 1px 0 oklch(1 0 0 / 0.07), inset 0 -2px 0 oklch(0 0 0 / 0.3), 0 3px 0 color-mix(in oklab, var(--color-svgb08-cap) 70%, #000), 0 8px 16px -10px oklch(0 0 0 / 0.7);
--svgb-08-key-sh-f: 0 0 0 3px color-mix(in oklab, var(--color-svgb08-stroke) 40%, transparent), 0 8px 0 color-mix(in oklab, var(--color-svgb08-cap) 70%, #000);
}
.svgb-08:has(#svgb-08-theme:checked) {
--color-svgb08-bg: #e8e9ec;
--color-svgb08-bg: oklch(0.935 0.004 250);
--color-svgb08-cap: #fbfbfc;
--color-svgb08-cap: oklch(0.985 0.002 250);
--color-svgb08-fg: #14171b;
--color-svgb08-fg: oklch(0.20 0.005 250);
--color-svgb08-muted: #5a6068;
--color-svgb08-muted: oklch(0.50 0.011 250);
--color-svgb08-rest: rgba(20, 23, 27, 0.16);
--color-svgb08-rest: oklch(0.20 0.005 250 / 0.16);
--color-svgb08-stroke: #3f6b00;
--color-svgb08-stroke: oklch(0.48 0.16 126);
--color-svgb08-warn: #b03a26;
--color-svgb08-warn: oklch(0.50 0.17 30);
--svgb-08-key-sh: inset 0 1px 0 oklch(1 0 0 / 0.9), inset 0 -3px 0 oklch(0 0 0 / 0.08), 0 8px 0 color-mix(in oklab, var(--color-svgb08-cap) 78%, #000 22%), 0 18px 30px -18px oklch(0 0 0 / 0.4);
}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-svgb08-bg: #17181b;
--color-svgb08-bg: oklch(0.225 0.004 250);
--color-svgb08-cap: #23252a;
--color-svgb08-cap: oklch(0.275 0.006 255);
--color-svgb08-fg: #e9ebee;
--color-svgb08-fg: oklch(0.94 0.004 250);
--color-svgb08-muted: #868b93;
--color-svgb08-muted: oklch(0.655 0.011 250);
--color-svgb08-rest: rgba(233, 235, 238, 0.14);
--color-svgb08-rest: oklch(0.94 0.004 250 / 0.14);
--color-svgb08-stroke: #c6ff4f;
--color-svgb08-stroke: oklch(0.92 0.20 122);
--color-svgb08-warn: #ff7a6b;
--color-svgb08-warn: oklch(0.75 0.16 30);
--font-svgb08-sans: 'Figtree', system-ui, sans-serif;
--font-svgb08-mono: 'JetBrains Mono', ui-monospace, monospace;
/* the wrong way, kept on purpose: a keyframe cannot resume from an
interrupted position, so this key snaps back on mouse-out */
--animate-svgb08-draw: svgb-08-draw 520ms cubic-bezier(0.45, 0, 0.55, 1) forwards;
}
@keyframes svgb-08-draw {
from {
stroke-dashoffset: 490;
}
to {
stroke-dashoffset: 0;
}
}
/* Four-layer keycap shadows, one set per interaction state. Kept as variables
so the markup reads [box-shadow:var(--svgb-08-key-sh)] rather than three
200-character arbitrary values, and so the light scheme swaps all of them. */
.svgb-08 {
--svgb-08-key-sh: inset 0 1px 0 oklch(1 0 0 / 0.09), inset 0 -3px 0 oklch(0 0 0 / 0.35), 0 8px 0 color-mix(in oklab, var(--color-svgb08-cap) 70%, #000), 0 18px 30px -14px oklch(0 0 0 / 0.7);
--svgb-08-key-sh-a: inset 0 1px 0 oklch(1 0 0 / 0.07), inset 0 -2px 0 oklch(0 0 0 / 0.3), 0 3px 0 color-mix(in oklab, var(--color-svgb08-cap) 70%, #000), 0 8px 16px -10px oklch(0 0 0 / 0.7);
--svgb-08-key-sh-f: 0 0 0 3px color-mix(in oklab, var(--color-svgb08-stroke) 40%, transparent), 0 8px 0 color-mix(in oklab, var(--color-svgb08-cap) 70%, #000);
}
.svgb-08:has(#svgb-08-theme:checked) {
--color-svgb08-bg: #e8e9ec;
--color-svgb08-bg: oklch(0.935 0.004 250);
--color-svgb08-cap: #fbfbfc;
--color-svgb08-cap: oklch(0.985 0.002 250);
--color-svgb08-fg: #14171b;
--color-svgb08-fg: oklch(0.20 0.005 250);
--color-svgb08-muted: #5a6068;
--color-svgb08-muted: oklch(0.50 0.011 250);
--color-svgb08-rest: rgba(20, 23, 27, 0.16);
--color-svgb08-rest: oklch(0.20 0.005 250 / 0.16);
--color-svgb08-stroke: #3f6b00;
--color-svgb08-stroke: oklch(0.48 0.16 126);
--color-svgb08-warn: #b03a26;
--color-svgb08-warn: oklch(0.50 0.17 30);
--svgb-08-key-sh: inset 0 1px 0 oklch(1 0 0 / 0.9), inset 0 -3px 0 oklch(0 0 0 / 0.08), 0 8px 0 color-mix(in oklab, var(--color-svgb08-cap) 78%, #000 22%), 0 18px 30px -18px oklch(0 0 0 / 0.4);
}@import "tailwindcss";
@theme {
--color-svgb08-bg: #17181b;
--color-svgb08-bg: oklch(0.225 0.004 250);
--color-svgb08-cap: #23252a;
--color-svgb08-cap: oklch(0.275 0.006 255);
--color-svgb08-fg: #e9ebee;
--color-svgb08-fg: oklch(0.94 0.004 250);
--color-svgb08-muted: #868b93;
--color-svgb08-muted: oklch(0.655 0.011 250);
--color-svgb08-rest: rgba(233, 235, 238, 0.14);
--color-svgb08-rest: oklch(0.94 0.004 250 / 0.14);
--color-svgb08-stroke: #c6ff4f;
--color-svgb08-stroke: oklch(0.92 0.20 122);
--color-svgb08-warn: #ff7a6b;
--color-svgb08-warn: oklch(0.75 0.16 30);
--font-svgb08-sans: 'Figtree', system-ui, sans-serif;
--font-svgb08-mono: 'JetBrains Mono', ui-monospace, monospace;
/* the wrong way, kept on purpose: a keyframe cannot resume from an
interrupted position, so this key snaps back on mouse-out */
--animate-svgb08-draw: svgb-08-draw 520ms cubic-bezier(0.45, 0, 0.55, 1) forwards;
}
@keyframes svgb-08-draw {
from {
stroke-dashoffset: 490;
}
to {
stroke-dashoffset: 0;
}
}
/* Four-layer keycap shadows, one set per interaction state. Kept as variables
so the markup reads [box-shadow:var(--svgb-08-key-sh)] rather than three
200-character arbitrary values, and so the light scheme swaps all of them. */
.svgb-08 {
--svgb-08-key-sh: inset 0 1px 0 oklch(1 0 0 / 0.09), inset 0 -3px 0 oklch(0 0 0 / 0.35), 0 8px 0 color-mix(in oklab, var(--color-svgb08-cap) 70%, #000), 0 18px 30px -14px oklch(0 0 0 / 0.7);
--svgb-08-key-sh-a: inset 0 1px 0 oklch(1 0 0 / 0.07), inset 0 -2px 0 oklch(0 0 0 / 0.3), 0 3px 0 color-mix(in oklab, var(--color-svgb08-cap) 70%, #000), 0 8px 16px -10px oklch(0 0 0 / 0.7);
--svgb-08-key-sh-f: 0 0 0 3px color-mix(in oklab, var(--color-svgb08-stroke) 40%, transparent), 0 8px 0 color-mix(in oklab, var(--color-svgb08-cap) 70%, #000);
}
.svgb-08:has(#svgb-08-theme:checked) {
--color-svgb08-bg: #e8e9ec;
--color-svgb08-bg: oklch(0.935 0.004 250);
--color-svgb08-cap: #fbfbfc;
--color-svgb08-cap: oklch(0.985 0.002 250);
--color-svgb08-fg: #14171b;
--color-svgb08-fg: oklch(0.20 0.005 250);
--color-svgb08-muted: #5a6068;
--color-svgb08-muted: oklch(0.50 0.011 250);
--color-svgb08-rest: rgba(20, 23, 27, 0.16);
--color-svgb08-rest: oklch(0.20 0.005 250 / 0.16);
--color-svgb08-stroke: #3f6b00;
--color-svgb08-stroke: oklch(0.48 0.16 126);
--color-svgb08-warn: #b03a26;
--color-svgb08-warn: oklch(0.50 0.17 30);
--svgb-08-key-sh: inset 0 1px 0 oklch(1 0 0 / 0.9), inset 0 -3px 0 oklch(0 0 0 / 0.08), 0 8px 0 color-mix(in oklab, var(--color-svgb08-cap) 78%, #000 22%), 0 18px 30px -18px oklch(0 0 0 / 0.4);
}<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=Figtree:wght@400;500;600&family=JetBrains+Mono:wght@400;500&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=Figtree:wght@400;500;600&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 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. - The whole lesson is three different transition declarations on three otherwise identical keys, and in Tailwind they sit side by side in the markup where you can compare them.
- The symmetric key is
transition-[stroke-dashoffset] duration-[520ms]; leaving mid-draw walks the offset back from wherever it reached. - The broken key uses
group-hover:animate-svgb08-drawinstead. An animation has no memory of an interrupted position, so it snaps — which is exactly what the demo is showing you. - The asymmetric key declares a full
[transition:...]twice: 760ms with a 40ms delay at rest, 300ms with no delay on hover. Twoduration-*utilities cannot express a per-direction curve. - Keycap shadows are four-layer and state-dependent, so they live in variables and the markup reads
[box-shadow:var(--svgb-08-key-sh)]— including theactive:andfocus-visible:swaps. motion-reduce:!animate-noneneeds the important flag here, because it has to beat agroup-hover:animation utility in the same layer.
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 Stroke Draw Button That Reverses on Mouse Leave
Source: https://codefronts.com/components/tailwind-svg-stroke-buttons/tailwind-stroke-draw-button-that-reverses-on-mouse-leave/
Three keycaps, three transition declarations, side by side in the markup where you can actually compare them. The symmetric key reverses from wherever the pointer left it; the broken key uses group-hover:animate-svgb08-draw and snaps, because an animation has no memory of an interrupted position; the third declares a full [transition:...] twice to get 300ms in and 760ms out.
## HTML
```html
<section class="svgb-08 relative block w-full min-h-svh bg-svgb08-bg text-svgb08-fg font-svgb08-sans">
<input class="peer absolute opacity-0 pointer-events-none" type="checkbox" id="svgb-08-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/15 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-08-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-content-center justify-items-center gap-[clamp(22px,4vh,40px)] p-[clamp(20px,5vw,70px)]">
<div class="flex flex-wrap justify-center gap-[clamp(18px,3vw,34px)]">
<div class="flex flex-col items-center gap-3.5">
<button class="group relative size-33 grid place-items-center border-0 p-0 rounded-[18px] [background:linear-gradient(180deg,color-mix(in_oklab,var(--color-svgb08-cap)_88%,#fff_12%),var(--color-svgb08-cap))] [box-shadow:var(--svgb-08-key-sh)] transition-[translate,box-shadow] duration-100 motion-reduce:transition-none active:translate-y-[5px] active:[box-shadow:var(--svgb-08-key-sh-a)] focus-visible:outline-none focus-visible:[box-shadow:var(--svgb-08-key-sh-f)]" type="button">
<svg class="absolute inset-0 w-full h-full overflow-visible" viewBox="0 0 132 132" aria-hidden="true">
<rect class="fill-none stroke-svgb08-rest [stroke-width:1.4] [stroke-linecap:round]" x="2" y="2" width="128" height="128" rx="18" vector-effect="non-scaling-stroke"/>
<rect class="fill-none [stroke-width:2.4] [stroke-linecap:round] [stroke-dasharray:490] [stroke-dashoffset:490] stroke-svgb08-stroke transition-[stroke-dashoffset] duration-[520ms] ease-[cubic-bezier(0.45,0,0.55,1)] group-hover:[stroke-dashoffset:0] group-focus-visible:[stroke-dashoffset:0] motion-reduce:!transition-none motion-reduce:!animate-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="128" height="128" rx="18" vector-effect="non-scaling-stroke"/>
</svg>
<span class="relative z-[1] flex flex-col items-center gap-2">
<span class="text-3xl leading-none transition-colors duration-300 motion-reduce:transition-none group-hover:text-svgb08-stroke group-focus-visible:text-svgb08-stroke">↶</span>
<span class="font-svgb08-mono text-[10.5px] tracking-[0.16em] uppercase text-svgb08-muted">reverse</span>
</span>
</button>
<span class="inline-flex items-center gap-[7px] font-svgb08-mono text-[11px] text-[color-mix(in_oklab,var(--color-svgb08-stroke)_62%,var(--color-svgb08-muted))] before:content-[''] before:size-1.5 before:rounded-full before:bg-current">transition · 520 / 520</span>
</div>
<div class="flex flex-col items-center gap-3.5">
<button class="group relative size-33 grid place-items-center border-0 p-0 rounded-[18px] [background:linear-gradient(180deg,color-mix(in_oklab,var(--color-svgb08-cap)_88%,#fff_12%),var(--color-svgb08-cap))] [box-shadow:var(--svgb-08-key-sh)] transition-[translate,box-shadow] duration-100 motion-reduce:transition-none active:translate-y-[5px] active:[box-shadow:var(--svgb-08-key-sh-a)] focus-visible:outline-none focus-visible:[box-shadow:var(--svgb-08-key-sh-f)]" type="button">
<svg class="absolute inset-0 w-full h-full overflow-visible" viewBox="0 0 132 132" aria-hidden="true">
<rect class="fill-none stroke-svgb08-rest [stroke-width:1.4] [stroke-linecap:round]" x="2" y="2" width="128" height="128" rx="18" vector-effect="non-scaling-stroke"/>
<rect class="fill-none [stroke-width:2.4] [stroke-linecap:round] [stroke-dasharray:490] [stroke-dashoffset:490] stroke-svgb08-warn group-hover:animate-svgb08-draw group-focus-visible:animate-svgb08-draw motion-reduce:!transition-none motion-reduce:!animate-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="128" height="128" rx="18" vector-effect="non-scaling-stroke"/>
</svg>
<span class="relative z-[1] flex flex-col items-center gap-2">
<span class="text-3xl leading-none transition-colors duration-300 motion-reduce:transition-none group-hover:text-svgb08-warn group-focus-visible:text-svgb08-warn">□</span>
<span class="font-svgb08-mono text-[10.5px] tracking-[0.16em] uppercase text-svgb08-muted">snap</span>
</span>
</button>
<span class="inline-flex items-center gap-[7px] font-svgb08-mono text-[11px] text-[color-mix(in_oklab,var(--color-svgb08-warn)_62%,var(--color-svgb08-muted))] before:content-[''] before:size-1.5 before:rounded-full before:bg-current">keyframes · no memory</span>
</div>
<div class="flex flex-col items-center gap-3.5">
<button class="group relative size-33 grid place-items-center border-0 p-0 rounded-[18px] [background:linear-gradient(180deg,color-mix(in_oklab,var(--color-svgb08-cap)_88%,#fff_12%),var(--color-svgb08-cap))] [box-shadow:var(--svgb-08-key-sh)] transition-[translate,box-shadow] duration-100 motion-reduce:transition-none active:translate-y-[5px] active:[box-shadow:var(--svgb-08-key-sh-a)] focus-visible:outline-none focus-visible:[box-shadow:var(--svgb-08-key-sh-f)]" type="button">
<svg class="absolute inset-0 w-full h-full overflow-visible" viewBox="0 0 132 132" aria-hidden="true">
<rect class="fill-none stroke-svgb08-rest [stroke-width:1.4] [stroke-linecap:round]" x="2" y="2" width="128" height="128" rx="18" vector-effect="non-scaling-stroke"/>
<rect class="fill-none [stroke-width:2.4] [stroke-linecap:round] [stroke-dasharray:490] [stroke-dashoffset:490] stroke-svgb08-stroke [transition:stroke-dashoffset_760ms_cubic-bezier(0.22,1,0.36,1)_40ms] group-hover:[stroke-dashoffset:0] group-hover:[transition:stroke-dashoffset_300ms_cubic-bezier(0.4,0,0.2,1)_0ms] group-focus-visible:[stroke-dashoffset:0] group-focus-visible:[transition:stroke-dashoffset_300ms_cubic-bezier(0.4,0,0.2,1)_0ms] motion-reduce:!transition-none motion-reduce:!animate-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="128" height="128" rx="18" vector-effect="non-scaling-stroke"/>
</svg>
<span class="relative z-[1] flex flex-col items-center gap-2">
<span class="text-3xl leading-none transition-colors duration-300 motion-reduce:transition-none group-hover:text-svgb08-stroke group-focus-visible:text-svgb08-stroke">⇥</span>
<span class="font-svgb08-mono text-[10.5px] tracking-[0.16em] uppercase text-svgb08-muted">asym</span>
</span>
</button>
<span class="inline-flex items-center gap-[7px] font-svgb08-mono text-[11px] text-[color-mix(in_oklab,var(--color-svgb08-stroke)_62%,var(--color-svgb08-muted))] before:content-[''] before:size-1.5 before:rounded-full before:bg-current">300 in · 760 out</span>
</div>
</div>
<div class="px-[18px] py-3 rounded-[10px] bg-svgb08-fg/4 border border-svgb08-rest">
<span class="font-svgb08-mono text-[11px] text-svgb08-muted">interrupt a draw halfway — only a transition can walk it back</span>
</div>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-svgb08-bg: #17181b;
--color-svgb08-bg: oklch(0.225 0.004 250);
--color-svgb08-cap: #23252a;
--color-svgb08-cap: oklch(0.275 0.006 255);
--color-svgb08-fg: #e9ebee;
--color-svgb08-fg: oklch(0.94 0.004 250);
--color-svgb08-muted: #868b93;
--color-svgb08-muted: oklch(0.655 0.011 250);
--color-svgb08-rest: rgba(233, 235, 238, 0.14);
--color-svgb08-rest: oklch(0.94 0.004 250 / 0.14);
--color-svgb08-stroke: #c6ff4f;
--color-svgb08-stroke: oklch(0.92 0.20 122);
--color-svgb08-warn: #ff7a6b;
--color-svgb08-warn: oklch(0.75 0.16 30);
--font-svgb08-sans: 'Figtree', system-ui, sans-serif;
--font-svgb08-mono: 'JetBrains Mono', ui-monospace, monospace;
/* the wrong way, kept on purpose: a keyframe cannot resume from an
interrupted position, so this key snaps back on mouse-out */
--animate-svgb08-draw: svgb-08-draw 520ms cubic-bezier(0.45, 0, 0.55, 1) forwards;
}
@keyframes svgb-08-draw {
from {
stroke-dashoffset: 490;
}
to {
stroke-dashoffset: 0;
}
}
/* Four-layer keycap shadows, one set per interaction state. Kept as variables
so the markup reads [box-shadow:var(--svgb-08-key-sh)] rather than three
200-character arbitrary values, and so the light scheme swaps all of them. */
.svgb-08 {
--svgb-08-key-sh: inset 0 1px 0 oklch(1 0 0 / 0.09), inset 0 -3px 0 oklch(0 0 0 / 0.35), 0 8px 0 color-mix(in oklab, var(--color-svgb08-cap) 70%, #000), 0 18px 30px -14px oklch(0 0 0 / 0.7);
--svgb-08-key-sh-a: inset 0 1px 0 oklch(1 0 0 / 0.07), inset 0 -2px 0 oklch(0 0 0 / 0.3), 0 3px 0 color-mix(in oklab, var(--color-svgb08-cap) 70%, #000), 0 8px 16px -10px oklch(0 0 0 / 0.7);
--svgb-08-key-sh-f: 0 0 0 3px color-mix(in oklab, var(--color-svgb08-stroke) 40%, transparent), 0 8px 0 color-mix(in oklab, var(--color-svgb08-cap) 70%, #000);
}
.svgb-08:has(#svgb-08-theme:checked) {
--color-svgb08-bg: #e8e9ec;
--color-svgb08-bg: oklch(0.935 0.004 250);
--color-svgb08-cap: #fbfbfc;
--color-svgb08-cap: oklch(0.985 0.002 250);
--color-svgb08-fg: #14171b;
--color-svgb08-fg: oklch(0.20 0.005 250);
--color-svgb08-muted: #5a6068;
--color-svgb08-muted: oklch(0.50 0.011 250);
--color-svgb08-rest: rgba(20, 23, 27, 0.16);
--color-svgb08-rest: oklch(0.20 0.005 250 / 0.16);
--color-svgb08-stroke: #3f6b00;
--color-svgb08-stroke: oklch(0.48 0.16 126);
--color-svgb08-warn: #b03a26;
--color-svgb08-warn: oklch(0.50 0.17 30);
--svgb-08-key-sh: inset 0 1px 0 oklch(1 0 0 / 0.9), inset 0 -3px 0 oklch(0 0 0 / 0.08), 0 8px 0 color-mix(in oklab, var(--color-svgb08-cap) 78%, #000 22%), 0 18px 30px -18px oklch(0 0 0 / 0.4);
}
```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 Stroke Draw Button That Reverses on Mouse Leave
Source: https://codefronts.com/components/tailwind-svg-stroke-buttons/tailwind-stroke-draw-button-that-reverses-on-mouse-leave/
Three keycaps, three transition declarations, side by side in the markup where you can actually compare them. The symmetric key reverses from wherever the pointer left it; the broken key uses group-hover:animate-svgb08-draw and snaps, because an animation has no memory of an interrupted position; the third declares a full [transition:...] twice to get 300ms in and 760ms out.
## HTML
```html
<section class="svgb-08 relative block w-full min-h-svh bg-svgb08-bg text-svgb08-fg font-svgb08-sans">
<input class="peer absolute opacity-0 pointer-events-none" type="checkbox" id="svgb-08-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/15 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-08-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-content-center justify-items-center gap-[clamp(22px,4vh,40px)] p-[clamp(20px,5vw,70px)]">
<div class="flex flex-wrap justify-center gap-[clamp(18px,3vw,34px)]">
<div class="flex flex-col items-center gap-3.5">
<button class="group relative size-33 grid place-items-center border-0 p-0 rounded-[18px] [background:linear-gradient(180deg,color-mix(in_oklab,var(--color-svgb08-cap)_88%,#fff_12%),var(--color-svgb08-cap))] [box-shadow:var(--svgb-08-key-sh)] transition-[translate,box-shadow] duration-100 motion-reduce:transition-none active:translate-y-[5px] active:[box-shadow:var(--svgb-08-key-sh-a)] focus-visible:outline-none focus-visible:[box-shadow:var(--svgb-08-key-sh-f)]" type="button">
<svg class="absolute inset-0 w-full h-full overflow-visible" viewBox="0 0 132 132" aria-hidden="true">
<rect class="fill-none stroke-svgb08-rest [stroke-width:1.4] [stroke-linecap:round]" x="2" y="2" width="128" height="128" rx="18" vector-effect="non-scaling-stroke"/>
<rect class="fill-none [stroke-width:2.4] [stroke-linecap:round] [stroke-dasharray:490] [stroke-dashoffset:490] stroke-svgb08-stroke transition-[stroke-dashoffset] duration-[520ms] ease-[cubic-bezier(0.45,0,0.55,1)] group-hover:[stroke-dashoffset:0] group-focus-visible:[stroke-dashoffset:0] motion-reduce:!transition-none motion-reduce:!animate-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="128" height="128" rx="18" vector-effect="non-scaling-stroke"/>
</svg>
<span class="relative z-[1] flex flex-col items-center gap-2">
<span class="text-3xl leading-none transition-colors duration-300 motion-reduce:transition-none group-hover:text-svgb08-stroke group-focus-visible:text-svgb08-stroke">↶</span>
<span class="font-svgb08-mono text-[10.5px] tracking-[0.16em] uppercase text-svgb08-muted">reverse</span>
</span>
</button>
<span class="inline-flex items-center gap-[7px] font-svgb08-mono text-[11px] text-[color-mix(in_oklab,var(--color-svgb08-stroke)_62%,var(--color-svgb08-muted))] before:content-[''] before:size-1.5 before:rounded-full before:bg-current">transition · 520 / 520</span>
</div>
<div class="flex flex-col items-center gap-3.5">
<button class="group relative size-33 grid place-items-center border-0 p-0 rounded-[18px] [background:linear-gradient(180deg,color-mix(in_oklab,var(--color-svgb08-cap)_88%,#fff_12%),var(--color-svgb08-cap))] [box-shadow:var(--svgb-08-key-sh)] transition-[translate,box-shadow] duration-100 motion-reduce:transition-none active:translate-y-[5px] active:[box-shadow:var(--svgb-08-key-sh-a)] focus-visible:outline-none focus-visible:[box-shadow:var(--svgb-08-key-sh-f)]" type="button">
<svg class="absolute inset-0 w-full h-full overflow-visible" viewBox="0 0 132 132" aria-hidden="true">
<rect class="fill-none stroke-svgb08-rest [stroke-width:1.4] [stroke-linecap:round]" x="2" y="2" width="128" height="128" rx="18" vector-effect="non-scaling-stroke"/>
<rect class="fill-none [stroke-width:2.4] [stroke-linecap:round] [stroke-dasharray:490] [stroke-dashoffset:490] stroke-svgb08-warn group-hover:animate-svgb08-draw group-focus-visible:animate-svgb08-draw motion-reduce:!transition-none motion-reduce:!animate-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="128" height="128" rx="18" vector-effect="non-scaling-stroke"/>
</svg>
<span class="relative z-[1] flex flex-col items-center gap-2">
<span class="text-3xl leading-none transition-colors duration-300 motion-reduce:transition-none group-hover:text-svgb08-warn group-focus-visible:text-svgb08-warn">□</span>
<span class="font-svgb08-mono text-[10.5px] tracking-[0.16em] uppercase text-svgb08-muted">snap</span>
</span>
</button>
<span class="inline-flex items-center gap-[7px] font-svgb08-mono text-[11px] text-[color-mix(in_oklab,var(--color-svgb08-warn)_62%,var(--color-svgb08-muted))] before:content-[''] before:size-1.5 before:rounded-full before:bg-current">keyframes · no memory</span>
</div>
<div class="flex flex-col items-center gap-3.5">
<button class="group relative size-33 grid place-items-center border-0 p-0 rounded-[18px] [background:linear-gradient(180deg,color-mix(in_oklab,var(--color-svgb08-cap)_88%,#fff_12%),var(--color-svgb08-cap))] [box-shadow:var(--svgb-08-key-sh)] transition-[translate,box-shadow] duration-100 motion-reduce:transition-none active:translate-y-[5px] active:[box-shadow:var(--svgb-08-key-sh-a)] focus-visible:outline-none focus-visible:[box-shadow:var(--svgb-08-key-sh-f)]" type="button">
<svg class="absolute inset-0 w-full h-full overflow-visible" viewBox="0 0 132 132" aria-hidden="true">
<rect class="fill-none stroke-svgb08-rest [stroke-width:1.4] [stroke-linecap:round]" x="2" y="2" width="128" height="128" rx="18" vector-effect="non-scaling-stroke"/>
<rect class="fill-none [stroke-width:2.4] [stroke-linecap:round] [stroke-dasharray:490] [stroke-dashoffset:490] stroke-svgb08-stroke [transition:stroke-dashoffset_760ms_cubic-bezier(0.22,1,0.36,1)_40ms] group-hover:[stroke-dashoffset:0] group-hover:[transition:stroke-dashoffset_300ms_cubic-bezier(0.4,0,0.2,1)_0ms] group-focus-visible:[stroke-dashoffset:0] group-focus-visible:[transition:stroke-dashoffset_300ms_cubic-bezier(0.4,0,0.2,1)_0ms] motion-reduce:!transition-none motion-reduce:!animate-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="128" height="128" rx="18" vector-effect="non-scaling-stroke"/>
</svg>
<span class="relative z-[1] flex flex-col items-center gap-2">
<span class="text-3xl leading-none transition-colors duration-300 motion-reduce:transition-none group-hover:text-svgb08-stroke group-focus-visible:text-svgb08-stroke">⇥</span>
<span class="font-svgb08-mono text-[10.5px] tracking-[0.16em] uppercase text-svgb08-muted">asym</span>
</span>
</button>
<span class="inline-flex items-center gap-[7px] font-svgb08-mono text-[11px] text-[color-mix(in_oklab,var(--color-svgb08-stroke)_62%,var(--color-svgb08-muted))] before:content-[''] before:size-1.5 before:rounded-full before:bg-current">300 in · 760 out</span>
</div>
</div>
<div class="px-[18px] py-3 rounded-[10px] bg-svgb08-fg/4 border border-svgb08-rest">
<span class="font-svgb08-mono text-[11px] text-svgb08-muted">interrupt a draw halfway — only a transition can walk it back</span>
</div>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-svgb08-bg: #17181b;
--color-svgb08-bg: oklch(0.225 0.004 250);
--color-svgb08-cap: #23252a;
--color-svgb08-cap: oklch(0.275 0.006 255);
--color-svgb08-fg: #e9ebee;
--color-svgb08-fg: oklch(0.94 0.004 250);
--color-svgb08-muted: #868b93;
--color-svgb08-muted: oklch(0.655 0.011 250);
--color-svgb08-rest: rgba(233, 235, 238, 0.14);
--color-svgb08-rest: oklch(0.94 0.004 250 / 0.14);
--color-svgb08-stroke: #c6ff4f;
--color-svgb08-stroke: oklch(0.92 0.20 122);
--color-svgb08-warn: #ff7a6b;
--color-svgb08-warn: oklch(0.75 0.16 30);
--font-svgb08-sans: 'Figtree', system-ui, sans-serif;
--font-svgb08-mono: 'JetBrains Mono', ui-monospace, monospace;
/* the wrong way, kept on purpose: a keyframe cannot resume from an
interrupted position, so this key snaps back on mouse-out */
--animate-svgb08-draw: svgb-08-draw 520ms cubic-bezier(0.45, 0, 0.55, 1) forwards;
}
@keyframes svgb-08-draw {
from {
stroke-dashoffset: 490;
}
to {
stroke-dashoffset: 0;
}
}
/* Four-layer keycap shadows, one set per interaction state. Kept as variables
so the markup reads [box-shadow:var(--svgb-08-key-sh)] rather than three
200-character arbitrary values, and so the light scheme swaps all of them. */
.svgb-08 {
--svgb-08-key-sh: inset 0 1px 0 oklch(1 0 0 / 0.09), inset 0 -3px 0 oklch(0 0 0 / 0.35), 0 8px 0 color-mix(in oklab, var(--color-svgb08-cap) 70%, #000), 0 18px 30px -14px oklch(0 0 0 / 0.7);
--svgb-08-key-sh-a: inset 0 1px 0 oklch(1 0 0 / 0.07), inset 0 -2px 0 oklch(0 0 0 / 0.3), 0 3px 0 color-mix(in oklab, var(--color-svgb08-cap) 70%, #000), 0 8px 16px -10px oklch(0 0 0 / 0.7);
--svgb-08-key-sh-f: 0 0 0 3px color-mix(in oklab, var(--color-svgb08-stroke) 40%, transparent), 0 8px 0 color-mix(in oklab, var(--color-svgb08-cap) 70%, #000);
}
.svgb-08:has(#svgb-08-theme:checked) {
--color-svgb08-bg: #e8e9ec;
--color-svgb08-bg: oklch(0.935 0.004 250);
--color-svgb08-cap: #fbfbfc;
--color-svgb08-cap: oklch(0.985 0.002 250);
--color-svgb08-fg: #14171b;
--color-svgb08-fg: oklch(0.20 0.005 250);
--color-svgb08-muted: #5a6068;
--color-svgb08-muted: oklch(0.50 0.011 250);
--color-svgb08-rest: rgba(20, 23, 27, 0.16);
--color-svgb08-rest: oklch(0.20 0.005 250 / 0.16);
--color-svgb08-stroke: #3f6b00;
--color-svgb08-stroke: oklch(0.48 0.16 126);
--color-svgb08-warn: #b03a26;
--color-svgb08-warn: oklch(0.50 0.17 30);
--svgb-08-key-sh: inset 0 1px 0 oklch(1 0 0 / 0.9), inset 0 -3px 0 oklch(0 0 0 / 0.08), 0 8px 0 color-mix(in oklab, var(--color-svgb08-cap) 78%, #000 22%), 0 18px 30px -18px oklch(0 0 0 / 0.4);
}
```How this works
A transition remembers; an animation does not. That is the entire demo, and the port makes it a two-line diff:
<!-- reverses from wherever it reached -->
class="transition-[stroke-dashoffset] duration-[520ms]
group-hover:[stroke-dashoffset:0]"
<!-- snaps back to the start -->
class="group-hover:animate-svgb08-draw"Per-direction timing needs the shorthand. The third key draws in 300ms and retracts in 760ms. A transition's timing is read from the element's current state, so the exit values belong on the resting rule and the entry values on the hover rule. duration-* can only set one number per declaration, so this key writes both out in full:
class="[transition:stroke-dashoffset_760ms_ease_40ms]
group-hover:[transition:stroke-dashoffset_300ms_ease]"Why the delay sits on the exit and not the entry. A 40ms hesitation before retracting absorbs pointer jitter at the edge of the key — the border does not flicker when you graze it. On entry the same delay would read as lag.
Shadows as variables, not utilities. A keycap needs four shadow layers, and they change on press and on focus. Four-layer values as arbitrary utilities would be unreadable, so they live in variables and the markup reads [box-shadow:var(--svgb-08-key-sh)] with active: and focus-visible: pointing at different variables.
One place the important flag is unavoidable. motion-reduce:animate-none and group-hover:animate-svgb08-draw are both animation utilities in the same layer, and the media query does not raise specificity. motion-reduce:!animate-none is the correct fix, not a workaround.
Make it yours
- Retime the symmetric key with one
duration-*; it stays reversible whatever the number. - Adjust the asymmetry by editing the two arbitrary shorthands. Exit slower than entry is the convention — the reverse feels impatient.
- Keep the broken key. Deleting it removes the comparison that makes the other two legible.
- Tune the 40ms exit delay up to about 80ms on large targets; past that, leaving feels sticky.
- Swap the shadow variables for a flat treatment if the keycaps sit on a busy ground — the stroke lesson survives without the moulding.
Gotchas — read before shipping
group-hover:animate-*restarts from frame zero every time. That is correct here because it is the point, but it is the wrong default for a hover reveal.- Transition timing is read from the element's current state, so per-direction curves need two declarations — not two
duration-*utilities. motion-reduce:animate-noneloses to agroup-hover:animate-*in the same layer. Add!.- An exit delay on the entry side reads as lag. Keep it on the resting declaration.
- Multi-layer shadows as arbitrary values need an underscore for every space and quickly become unmaintainable; variables are the right tool.
- Do not mix a transition and an animation on the same property. The animation wins while it runs and the transition appears broken for reasons nothing in the markup explains.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
Transitions, animations and their interruption behaviour are ancient and consistent. The floor is Tailwind v4's generated sheet; the demo's lesson — that an interrupted animation snaps — is true in every engine ever shipped.