20 / 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 Variable Width Stroke Button with Text Length
Six labels from three characters to thirty-five, each deriving its stroke weight, dash rhythm and draw duration from one inline number. Hover transitions the dash pattern rather than the offset — hover:[--svgb-20-duty:1] closes every gap until the measure becomes a solid border, a state change no dash-offset draw can express. Both @property blocks stay in CSS, and they are the load-bearing part.
Nucleo: 44,000+ SVG icons in five families · All Icons Bundle $149
Nucleo · Affiliate link
The code
<section class="svgb-20 relative block w-full min-h-svh bg-svgb20-bg text-svgb20-fg font-svgb20-sans">
<input class="peer absolute opacity-0 pointer-events-none" type="checkbox" id="svgb-20-theme">
<label class="absolute z-[5] top-4 right-4 grid place-items-center size-11 rounded-sm border border-current/22 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-20-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(22px,5vw,68px)]">
<div class="w-full max-w-225 flex flex-col gap-[clamp(20px,3vw,32px)]">
<div class="flex flex-wrap items-end justify-between gap-3.5 pb-4 border-b border-svgb20-rest">
<h1 class="m-0 text-[clamp(38px,7vw,82px)] font-bold [font-stretch:118%] leading-[0.88] tracking-[-0.045em] uppercase">Weight<br>Ladder</h1>
<p class="m-0 font-svgb20-mono text-[11px] tracking-[0.02em] text-svgb20-muted">stroke-width: clamp(1.5px, 1.1px + chars × 0.055px, 3px)</p>
</div>
<ol class="list-none m-0 p-0 flex flex-col gap-[clamp(12px,2vw,18px)]">
<li class="grid grid-cols-[34px_1fr_54px] gap-[clamp(12px,2.5vw,22px)] items-center">
<span class="font-svgb20-mono text-[11px] tabular-nums text-svgb20-muted">03</span>
<button class="group relative inline-grid place-items-center justify-self-start min-w-24 min-h-[62px] px-[clamp(20px,2.6vw,32px)] border-0 rounded bg-transparent font-normal tracking-[-0.01em] text-[clamp(15px,calc(14px+var(--svgb-20-chars)*0.04px),18px)] cursor-pointer [transition:--svgb-20-duty_calc(340ms+var(--svgb-20-chars)*14ms)_cubic-bezier(0.22,1,0.36,1)] hover:[--svgb-20-duty:1] focus-visible:[--svgb-20-duty:1] motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-svgb20-stroke focus-visible:outline-offset-[3px]" type="button" data-chars="3" style="--svgb-20-chars: 3">
<svg class="absolute inset-0 w-full h-full overflow-visible" aria-hidden="true"><rect class="svgb-20__ink fill-none [x:1.5px] [y:1.5px] [width:calc(100%-3px)] [height:calc(100%-3px)] [rx:4px] stroke-svgb20-rest [stroke-linecap:butt] [stroke-dasharray:calc(var(--svgb-20-duty)/var(--svgb-20-chars))_calc((1-var(--svgb-20-duty))/var(--svgb-20-chars))] transition-[stroke] duration-[240ms] group-hover:stroke-svgb20-stroke group-focus-visible:stroke-svgb20-stroke motion-reduce:[stroke-dasharray:none] [stroke-width:clamp(1.5px,calc(1.1px+var(--svgb-20-chars)*0.055px),3px)]" data-chars="3" pathLength="1" vector-effect="non-scaling-stroke"/></svg>
<span class="relative z-[1] whitespace-nowrap transition-colors duration-[260ms] motion-reduce:transition-none group-hover:text-svgb20-stroke group-focus-visible:text-svgb20-stroke">Buy</span>
</button>
<span class="font-svgb20-mono text-[11px] tabular-nums text-svgb20-muted text-end">1.5px</span>
</li>
<li class="grid grid-cols-[34px_1fr_54px] gap-[clamp(12px,2.5vw,22px)] items-center">
<span class="font-svgb20-mono text-[11px] tabular-nums text-svgb20-muted">08</span>
<button class="group relative inline-grid place-items-center justify-self-start min-w-24 min-h-[62px] px-[clamp(20px,2.6vw,32px)] border-0 rounded bg-transparent font-normal tracking-[-0.01em] text-[clamp(15px,calc(14px+var(--svgb-20-chars)*0.04px),18px)] cursor-pointer [transition:--svgb-20-duty_calc(340ms+var(--svgb-20-chars)*14ms)_cubic-bezier(0.22,1,0.36,1)] hover:[--svgb-20-duty:1] focus-visible:[--svgb-20-duty:1] motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-svgb20-stroke focus-visible:outline-offset-[3px]" type="button" data-chars="8" style="--svgb-20-chars: 8">
<svg class="absolute inset-0 w-full h-full overflow-visible" aria-hidden="true"><rect class="svgb-20__ink fill-none [x:1.5px] [y:1.5px] [width:calc(100%-3px)] [height:calc(100%-3px)] [rx:4px] stroke-svgb20-rest [stroke-linecap:butt] [stroke-dasharray:calc(var(--svgb-20-duty)/var(--svgb-20-chars))_calc((1-var(--svgb-20-duty))/var(--svgb-20-chars))] transition-[stroke] duration-[240ms] group-hover:stroke-svgb20-stroke group-focus-visible:stroke-svgb20-stroke motion-reduce:[stroke-dasharray:none] [stroke-width:clamp(1.5px,calc(1.1px+var(--svgb-20-chars)*0.055px),3px)]" data-chars="8" pathLength="1" vector-effect="non-scaling-stroke"/></svg>
<span class="relative z-[1] whitespace-nowrap transition-colors duration-[260ms] motion-reduce:transition-none group-hover:text-svgb20-stroke group-focus-visible:text-svgb20-stroke">Add note</span>
</button>
<span class="font-svgb20-mono text-[11px] tabular-nums text-svgb20-muted text-end">1.5px</span>
</li>
<li class="grid grid-cols-[34px_1fr_54px] gap-[clamp(12px,2.5vw,22px)] items-center">
<span class="font-svgb20-mono text-[11px] tabular-nums text-svgb20-muted">13</span>
<button class="group relative inline-grid place-items-center justify-self-start min-w-24 min-h-[62px] px-[clamp(20px,2.6vw,32px)] border-0 rounded bg-transparent font-normal tracking-[-0.01em] text-[clamp(15px,calc(14px+var(--svgb-20-chars)*0.04px),18px)] cursor-pointer [transition:--svgb-20-duty_calc(340ms+var(--svgb-20-chars)*14ms)_cubic-bezier(0.22,1,0.36,1)] hover:[--svgb-20-duty:1] focus-visible:[--svgb-20-duty:1] motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-svgb20-stroke focus-visible:outline-offset-[3px]" type="button" data-chars="13" style="--svgb-20-chars: 13">
<svg class="absolute inset-0 w-full h-full overflow-visible" aria-hidden="true"><rect class="svgb-20__ink fill-none [x:1.5px] [y:1.5px] [width:calc(100%-3px)] [height:calc(100%-3px)] [rx:4px] stroke-svgb20-rest [stroke-linecap:butt] [stroke-dasharray:calc(var(--svgb-20-duty)/var(--svgb-20-chars))_calc((1-var(--svgb-20-duty))/var(--svgb-20-chars))] transition-[stroke] duration-[240ms] group-hover:stroke-svgb20-stroke group-focus-visible:stroke-svgb20-stroke motion-reduce:[stroke-dasharray:none] [stroke-width:clamp(1.5px,calc(1.1px+var(--svgb-20-chars)*0.055px),3px)]" data-chars="13" pathLength="1" vector-effect="non-scaling-stroke"/></svg>
<span class="relative z-[1] whitespace-nowrap transition-colors duration-[260ms] motion-reduce:transition-none group-hover:text-svgb20-stroke group-focus-visible:text-svgb20-stroke">Export as CSV</span>
</button>
<span class="font-svgb20-mono text-[11px] tabular-nums text-svgb20-muted text-end">1.8px</span>
</li>
<li class="grid grid-cols-[34px_1fr_54px] gap-[clamp(12px,2.5vw,22px)] items-center">
<span class="font-svgb20-mono text-[11px] tabular-nums text-svgb20-muted">22</span>
<button class="group relative inline-grid place-items-center justify-self-start min-w-24 min-h-[62px] px-[clamp(20px,2.6vw,32px)] border-0 rounded bg-transparent font-normal tracking-[-0.01em] text-[clamp(15px,calc(14px+var(--svgb-20-chars)*0.04px),18px)] cursor-pointer [transition:--svgb-20-duty_calc(340ms+var(--svgb-20-chars)*14ms)_cubic-bezier(0.22,1,0.36,1)] hover:[--svgb-20-duty:1] focus-visible:[--svgb-20-duty:1] motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-svgb20-stroke focus-visible:outline-offset-[3px]" type="button" data-chars="22" style="--svgb-20-chars: 22">
<svg class="absolute inset-0 w-full h-full overflow-visible" aria-hidden="true"><rect class="svgb-20__ink fill-none [x:1.5px] [y:1.5px] [width:calc(100%-3px)] [height:calc(100%-3px)] [rx:4px] stroke-svgb20-rest [stroke-linecap:butt] [stroke-dasharray:calc(var(--svgb-20-duty)/var(--svgb-20-chars))_calc((1-var(--svgb-20-duty))/var(--svgb-20-chars))] transition-[stroke] duration-[240ms] group-hover:stroke-svgb20-stroke group-focus-visible:stroke-svgb20-stroke motion-reduce:[stroke-dasharray:none] [stroke-width:clamp(1.5px,calc(1.1px+var(--svgb-20-chars)*0.055px),3px)]" data-chars="22" pathLength="1" vector-effect="non-scaling-stroke"/></svg>
<span class="relative z-[1] whitespace-nowrap transition-colors duration-[260ms] motion-reduce:transition-none group-hover:text-svgb20-stroke group-focus-visible:text-svgb20-stroke">Schedule for next week</span>
</button>
<span class="font-svgb20-mono text-[11px] tabular-nums text-svgb20-muted text-end">2.3px</span>
</li>
<li class="grid grid-cols-[34px_1fr_54px] gap-[clamp(12px,2.5vw,22px)] items-center">
<span class="font-svgb20-mono text-[11px] tabular-nums text-svgb20-muted">35</span>
<button class="group relative inline-grid place-items-center justify-self-start min-w-24 min-h-[62px] px-[clamp(20px,2.6vw,32px)] border-0 rounded bg-transparent font-normal tracking-[-0.01em] text-[clamp(15px,calc(14px+var(--svgb-20-chars)*0.04px),18px)] cursor-pointer [transition:--svgb-20-duty_calc(340ms+var(--svgb-20-chars)*14ms)_cubic-bezier(0.22,1,0.36,1)] hover:[--svgb-20-duty:1] focus-visible:[--svgb-20-duty:1] motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-svgb20-stroke focus-visible:outline-offset-[3px]" type="button" data-chars="35" style="--svgb-20-chars: 35">
<svg class="absolute inset-0 w-full h-full overflow-visible" aria-hidden="true"><rect class="svgb-20__ink fill-none [x:1.5px] [y:1.5px] [width:calc(100%-3px)] [height:calc(100%-3px)] [rx:4px] stroke-svgb20-rest [stroke-linecap:butt] [stroke-dasharray:calc(var(--svgb-20-duty)/var(--svgb-20-chars))_calc((1-var(--svgb-20-duty))/var(--svgb-20-chars))] transition-[stroke] duration-[240ms] group-hover:stroke-svgb20-stroke group-focus-visible:stroke-svgb20-stroke motion-reduce:[stroke-dasharray:none] [stroke-width:clamp(1.5px,calc(1.1px+var(--svgb-20-chars)*0.055px),3px)]" data-chars="35" pathLength="1" vector-effect="non-scaling-stroke"/></svg>
<span class="relative z-[1] whitespace-nowrap transition-colors duration-[260ms] motion-reduce:transition-none group-hover:text-svgb20-stroke group-focus-visible:text-svgb20-stroke">Duplicate workspace and invite team</span>
</button>
<span class="font-svgb20-mono text-[11px] tabular-nums text-svgb20-muted text-end">3.0px</span>
</li>
</ol>
<div class="flex flex-col gap-3 pt-[18px] border-t border-svgb20-rest">
<span class="font-svgb20-mono text-[10.5px] tracking-[0.16em] uppercase text-svgb20-muted">control · fixed 2px</span>
<div class="flex flex-wrap items-center gap-3.5">
<button class="group relative inline-grid place-items-center justify-self-start min-w-24 min-h-[62px] px-[clamp(20px,2.6vw,32px)] border-0 rounded bg-transparent font-normal tracking-[-0.01em] text-[clamp(15px,calc(14px+var(--svgb-20-chars)*0.04px),18px)] cursor-pointer [transition:--svgb-20-duty_calc(340ms+var(--svgb-20-chars)*14ms)_cubic-bezier(0.22,1,0.36,1)] hover:[--svgb-20-duty:1] focus-visible:[--svgb-20-duty:1] motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-svgb20-stroke focus-visible:outline-offset-[3px] text-svgb20-muted" type="button" data-chars="3" style="--svgb-20-chars: 3">
<svg class="absolute inset-0 w-full h-full overflow-visible" aria-hidden="true"><rect class="svgb-20__ink fill-none [x:1.5px] [y:1.5px] [width:calc(100%-3px)] [height:calc(100%-3px)] [rx:4px] stroke-svgb20-rest [stroke-linecap:butt] [stroke-dasharray:calc(var(--svgb-20-duty)/var(--svgb-20-chars))_calc((1-var(--svgb-20-duty))/var(--svgb-20-chars))] transition-[stroke] duration-[240ms] group-hover:stroke-svgb20-stroke group-focus-visible:stroke-svgb20-stroke motion-reduce:[stroke-dasharray:none] [stroke-width:2px] [transition-duration:560ms]" data-chars="3" pathLength="1" vector-effect="non-scaling-stroke"/></svg>
<span class="relative z-[1] whitespace-nowrap transition-colors duration-[260ms] motion-reduce:transition-none group-hover:text-svgb20-stroke group-focus-visible:text-svgb20-stroke">Buy</span>
</button>
<button class="group relative inline-grid place-items-center justify-self-start min-w-24 min-h-[62px] px-[clamp(20px,2.6vw,32px)] border-0 rounded bg-transparent font-normal tracking-[-0.01em] text-[clamp(15px,calc(14px+var(--svgb-20-chars)*0.04px),18px)] cursor-pointer [transition:--svgb-20-duty_calc(340ms+var(--svgb-20-chars)*14ms)_cubic-bezier(0.22,1,0.36,1)] hover:[--svgb-20-duty:1] focus-visible:[--svgb-20-duty:1] motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-svgb20-stroke focus-visible:outline-offset-[3px] text-svgb20-muted" type="button" data-chars="35" style="--svgb-20-chars: 35">
<svg class="absolute inset-0 w-full h-full overflow-visible" aria-hidden="true"><rect class="svgb-20__ink fill-none [x:1.5px] [y:1.5px] [width:calc(100%-3px)] [height:calc(100%-3px)] [rx:4px] stroke-svgb20-rest [stroke-linecap:butt] [stroke-dasharray:calc(var(--svgb-20-duty)/var(--svgb-20-chars))_calc((1-var(--svgb-20-duty))/var(--svgb-20-chars))] transition-[stroke] duration-[240ms] group-hover:stroke-svgb20-stroke group-focus-visible:stroke-svgb20-stroke motion-reduce:[stroke-dasharray:none] [stroke-width:2px] [transition-duration:560ms]" data-chars="35" pathLength="1" vector-effect="non-scaling-stroke"/></svg>
<span class="relative z-[1] whitespace-nowrap transition-colors duration-[260ms] motion-reduce:transition-none group-hover:text-svgb20-stroke group-focus-visible:text-svgb20-stroke">Duplicate workspace and invite team</span>
</button>
</div>
</div>
</div>
</div>
</section><section class="svgb-20 relative block w-full min-h-svh bg-svgb20-bg text-svgb20-fg font-svgb20-sans">
<input class="peer absolute opacity-0 pointer-events-none" type="checkbox" id="svgb-20-theme">
<label class="absolute z-[5] top-4 right-4 grid place-items-center size-11 rounded-sm border border-current/22 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-20-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(22px,5vw,68px)]">
<div class="w-full max-w-225 flex flex-col gap-[clamp(20px,3vw,32px)]">
<div class="flex flex-wrap items-end justify-between gap-3.5 pb-4 border-b border-svgb20-rest">
<h1 class="m-0 text-[clamp(38px,7vw,82px)] font-bold [font-stretch:118%] leading-[0.88] tracking-[-0.045em] uppercase">Weight<br>Ladder</h1>
<p class="m-0 font-svgb20-mono text-[11px] tracking-[0.02em] text-svgb20-muted">stroke-width: clamp(1.5px, 1.1px + chars × 0.055px, 3px)</p>
</div>
<ol class="list-none m-0 p-0 flex flex-col gap-[clamp(12px,2vw,18px)]">
<li class="grid grid-cols-[34px_1fr_54px] gap-[clamp(12px,2.5vw,22px)] items-center">
<span class="font-svgb20-mono text-[11px] tabular-nums text-svgb20-muted">03</span>
<button class="group relative inline-grid place-items-center justify-self-start min-w-24 min-h-[62px] px-[clamp(20px,2.6vw,32px)] border-0 rounded bg-transparent font-normal tracking-[-0.01em] text-[clamp(15px,calc(14px+var(--svgb-20-chars)*0.04px),18px)] cursor-pointer [transition:--svgb-20-duty_calc(340ms+var(--svgb-20-chars)*14ms)_cubic-bezier(0.22,1,0.36,1)] hover:[--svgb-20-duty:1] focus-visible:[--svgb-20-duty:1] motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-svgb20-stroke focus-visible:outline-offset-[3px]" type="button" data-chars="3" style="--svgb-20-chars: 3">
<svg class="absolute inset-0 w-full h-full overflow-visible" aria-hidden="true"><rect class="svgb-20__ink fill-none [x:1.5px] [y:1.5px] [width:calc(100%-3px)] [height:calc(100%-3px)] [rx:4px] stroke-svgb20-rest [stroke-linecap:butt] [stroke-dasharray:calc(var(--svgb-20-duty)/var(--svgb-20-chars))_calc((1-var(--svgb-20-duty))/var(--svgb-20-chars))] transition-[stroke] duration-[240ms] group-hover:stroke-svgb20-stroke group-focus-visible:stroke-svgb20-stroke motion-reduce:[stroke-dasharray:none] [stroke-width:clamp(1.5px,calc(1.1px+var(--svgb-20-chars)*0.055px),3px)]" data-chars="3" pathLength="1" vector-effect="non-scaling-stroke"/></svg>
<span class="relative z-[1] whitespace-nowrap transition-colors duration-[260ms] motion-reduce:transition-none group-hover:text-svgb20-stroke group-focus-visible:text-svgb20-stroke">Buy</span>
</button>
<span class="font-svgb20-mono text-[11px] tabular-nums text-svgb20-muted text-end">1.5px</span>
</li>
<li class="grid grid-cols-[34px_1fr_54px] gap-[clamp(12px,2.5vw,22px)] items-center">
<span class="font-svgb20-mono text-[11px] tabular-nums text-svgb20-muted">08</span>
<button class="group relative inline-grid place-items-center justify-self-start min-w-24 min-h-[62px] px-[clamp(20px,2.6vw,32px)] border-0 rounded bg-transparent font-normal tracking-[-0.01em] text-[clamp(15px,calc(14px+var(--svgb-20-chars)*0.04px),18px)] cursor-pointer [transition:--svgb-20-duty_calc(340ms+var(--svgb-20-chars)*14ms)_cubic-bezier(0.22,1,0.36,1)] hover:[--svgb-20-duty:1] focus-visible:[--svgb-20-duty:1] motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-svgb20-stroke focus-visible:outline-offset-[3px]" type="button" data-chars="8" style="--svgb-20-chars: 8">
<svg class="absolute inset-0 w-full h-full overflow-visible" aria-hidden="true"><rect class="svgb-20__ink fill-none [x:1.5px] [y:1.5px] [width:calc(100%-3px)] [height:calc(100%-3px)] [rx:4px] stroke-svgb20-rest [stroke-linecap:butt] [stroke-dasharray:calc(var(--svgb-20-duty)/var(--svgb-20-chars))_calc((1-var(--svgb-20-duty))/var(--svgb-20-chars))] transition-[stroke] duration-[240ms] group-hover:stroke-svgb20-stroke group-focus-visible:stroke-svgb20-stroke motion-reduce:[stroke-dasharray:none] [stroke-width:clamp(1.5px,calc(1.1px+var(--svgb-20-chars)*0.055px),3px)]" data-chars="8" pathLength="1" vector-effect="non-scaling-stroke"/></svg>
<span class="relative z-[1] whitespace-nowrap transition-colors duration-[260ms] motion-reduce:transition-none group-hover:text-svgb20-stroke group-focus-visible:text-svgb20-stroke">Add note</span>
</button>
<span class="font-svgb20-mono text-[11px] tabular-nums text-svgb20-muted text-end">1.5px</span>
</li>
<li class="grid grid-cols-[34px_1fr_54px] gap-[clamp(12px,2.5vw,22px)] items-center">
<span class="font-svgb20-mono text-[11px] tabular-nums text-svgb20-muted">13</span>
<button class="group relative inline-grid place-items-center justify-self-start min-w-24 min-h-[62px] px-[clamp(20px,2.6vw,32px)] border-0 rounded bg-transparent font-normal tracking-[-0.01em] text-[clamp(15px,calc(14px+var(--svgb-20-chars)*0.04px),18px)] cursor-pointer [transition:--svgb-20-duty_calc(340ms+var(--svgb-20-chars)*14ms)_cubic-bezier(0.22,1,0.36,1)] hover:[--svgb-20-duty:1] focus-visible:[--svgb-20-duty:1] motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-svgb20-stroke focus-visible:outline-offset-[3px]" type="button" data-chars="13" style="--svgb-20-chars: 13">
<svg class="absolute inset-0 w-full h-full overflow-visible" aria-hidden="true"><rect class="svgb-20__ink fill-none [x:1.5px] [y:1.5px] [width:calc(100%-3px)] [height:calc(100%-3px)] [rx:4px] stroke-svgb20-rest [stroke-linecap:butt] [stroke-dasharray:calc(var(--svgb-20-duty)/var(--svgb-20-chars))_calc((1-var(--svgb-20-duty))/var(--svgb-20-chars))] transition-[stroke] duration-[240ms] group-hover:stroke-svgb20-stroke group-focus-visible:stroke-svgb20-stroke motion-reduce:[stroke-dasharray:none] [stroke-width:clamp(1.5px,calc(1.1px+var(--svgb-20-chars)*0.055px),3px)]" data-chars="13" pathLength="1" vector-effect="non-scaling-stroke"/></svg>
<span class="relative z-[1] whitespace-nowrap transition-colors duration-[260ms] motion-reduce:transition-none group-hover:text-svgb20-stroke group-focus-visible:text-svgb20-stroke">Export as CSV</span>
</button>
<span class="font-svgb20-mono text-[11px] tabular-nums text-svgb20-muted text-end">1.8px</span>
</li>
<li class="grid grid-cols-[34px_1fr_54px] gap-[clamp(12px,2.5vw,22px)] items-center">
<span class="font-svgb20-mono text-[11px] tabular-nums text-svgb20-muted">22</span>
<button class="group relative inline-grid place-items-center justify-self-start min-w-24 min-h-[62px] px-[clamp(20px,2.6vw,32px)] border-0 rounded bg-transparent font-normal tracking-[-0.01em] text-[clamp(15px,calc(14px+var(--svgb-20-chars)*0.04px),18px)] cursor-pointer [transition:--svgb-20-duty_calc(340ms+var(--svgb-20-chars)*14ms)_cubic-bezier(0.22,1,0.36,1)] hover:[--svgb-20-duty:1] focus-visible:[--svgb-20-duty:1] motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-svgb20-stroke focus-visible:outline-offset-[3px]" type="button" data-chars="22" style="--svgb-20-chars: 22">
<svg class="absolute inset-0 w-full h-full overflow-visible" aria-hidden="true"><rect class="svgb-20__ink fill-none [x:1.5px] [y:1.5px] [width:calc(100%-3px)] [height:calc(100%-3px)] [rx:4px] stroke-svgb20-rest [stroke-linecap:butt] [stroke-dasharray:calc(var(--svgb-20-duty)/var(--svgb-20-chars))_calc((1-var(--svgb-20-duty))/var(--svgb-20-chars))] transition-[stroke] duration-[240ms] group-hover:stroke-svgb20-stroke group-focus-visible:stroke-svgb20-stroke motion-reduce:[stroke-dasharray:none] [stroke-width:clamp(1.5px,calc(1.1px+var(--svgb-20-chars)*0.055px),3px)]" data-chars="22" pathLength="1" vector-effect="non-scaling-stroke"/></svg>
<span class="relative z-[1] whitespace-nowrap transition-colors duration-[260ms] motion-reduce:transition-none group-hover:text-svgb20-stroke group-focus-visible:text-svgb20-stroke">Schedule for next week</span>
</button>
<span class="font-svgb20-mono text-[11px] tabular-nums text-svgb20-muted text-end">2.3px</span>
</li>
<li class="grid grid-cols-[34px_1fr_54px] gap-[clamp(12px,2.5vw,22px)] items-center">
<span class="font-svgb20-mono text-[11px] tabular-nums text-svgb20-muted">35</span>
<button class="group relative inline-grid place-items-center justify-self-start min-w-24 min-h-[62px] px-[clamp(20px,2.6vw,32px)] border-0 rounded bg-transparent font-normal tracking-[-0.01em] text-[clamp(15px,calc(14px+var(--svgb-20-chars)*0.04px),18px)] cursor-pointer [transition:--svgb-20-duty_calc(340ms+var(--svgb-20-chars)*14ms)_cubic-bezier(0.22,1,0.36,1)] hover:[--svgb-20-duty:1] focus-visible:[--svgb-20-duty:1] motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-svgb20-stroke focus-visible:outline-offset-[3px]" type="button" data-chars="35" style="--svgb-20-chars: 35">
<svg class="absolute inset-0 w-full h-full overflow-visible" aria-hidden="true"><rect class="svgb-20__ink fill-none [x:1.5px] [y:1.5px] [width:calc(100%-3px)] [height:calc(100%-3px)] [rx:4px] stroke-svgb20-rest [stroke-linecap:butt] [stroke-dasharray:calc(var(--svgb-20-duty)/var(--svgb-20-chars))_calc((1-var(--svgb-20-duty))/var(--svgb-20-chars))] transition-[stroke] duration-[240ms] group-hover:stroke-svgb20-stroke group-focus-visible:stroke-svgb20-stroke motion-reduce:[stroke-dasharray:none] [stroke-width:clamp(1.5px,calc(1.1px+var(--svgb-20-chars)*0.055px),3px)]" data-chars="35" pathLength="1" vector-effect="non-scaling-stroke"/></svg>
<span class="relative z-[1] whitespace-nowrap transition-colors duration-[260ms] motion-reduce:transition-none group-hover:text-svgb20-stroke group-focus-visible:text-svgb20-stroke">Duplicate workspace and invite team</span>
</button>
<span class="font-svgb20-mono text-[11px] tabular-nums text-svgb20-muted text-end">3.0px</span>
</li>
</ol>
<div class="flex flex-col gap-3 pt-[18px] border-t border-svgb20-rest">
<span class="font-svgb20-mono text-[10.5px] tracking-[0.16em] uppercase text-svgb20-muted">control · fixed 2px</span>
<div class="flex flex-wrap items-center gap-3.5">
<button class="group relative inline-grid place-items-center justify-self-start min-w-24 min-h-[62px] px-[clamp(20px,2.6vw,32px)] border-0 rounded bg-transparent font-normal tracking-[-0.01em] text-[clamp(15px,calc(14px+var(--svgb-20-chars)*0.04px),18px)] cursor-pointer [transition:--svgb-20-duty_calc(340ms+var(--svgb-20-chars)*14ms)_cubic-bezier(0.22,1,0.36,1)] hover:[--svgb-20-duty:1] focus-visible:[--svgb-20-duty:1] motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-svgb20-stroke focus-visible:outline-offset-[3px] text-svgb20-muted" type="button" data-chars="3" style="--svgb-20-chars: 3">
<svg class="absolute inset-0 w-full h-full overflow-visible" aria-hidden="true"><rect class="svgb-20__ink fill-none [x:1.5px] [y:1.5px] [width:calc(100%-3px)] [height:calc(100%-3px)] [rx:4px] stroke-svgb20-rest [stroke-linecap:butt] [stroke-dasharray:calc(var(--svgb-20-duty)/var(--svgb-20-chars))_calc((1-var(--svgb-20-duty))/var(--svgb-20-chars))] transition-[stroke] duration-[240ms] group-hover:stroke-svgb20-stroke group-focus-visible:stroke-svgb20-stroke motion-reduce:[stroke-dasharray:none] [stroke-width:2px] [transition-duration:560ms]" data-chars="3" pathLength="1" vector-effect="non-scaling-stroke"/></svg>
<span class="relative z-[1] whitespace-nowrap transition-colors duration-[260ms] motion-reduce:transition-none group-hover:text-svgb20-stroke group-focus-visible:text-svgb20-stroke">Buy</span>
</button>
<button class="group relative inline-grid place-items-center justify-self-start min-w-24 min-h-[62px] px-[clamp(20px,2.6vw,32px)] border-0 rounded bg-transparent font-normal tracking-[-0.01em] text-[clamp(15px,calc(14px+var(--svgb-20-chars)*0.04px),18px)] cursor-pointer [transition:--svgb-20-duty_calc(340ms+var(--svgb-20-chars)*14ms)_cubic-bezier(0.22,1,0.36,1)] hover:[--svgb-20-duty:1] focus-visible:[--svgb-20-duty:1] motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-svgb20-stroke focus-visible:outline-offset-[3px] text-svgb20-muted" type="button" data-chars="35" style="--svgb-20-chars: 35">
<svg class="absolute inset-0 w-full h-full overflow-visible" aria-hidden="true"><rect class="svgb-20__ink fill-none [x:1.5px] [y:1.5px] [width:calc(100%-3px)] [height:calc(100%-3px)] [rx:4px] stroke-svgb20-rest [stroke-linecap:butt] [stroke-dasharray:calc(var(--svgb-20-duty)/var(--svgb-20-chars))_calc((1-var(--svgb-20-duty))/var(--svgb-20-chars))] transition-[stroke] duration-[240ms] group-hover:stroke-svgb20-stroke group-focus-visible:stroke-svgb20-stroke motion-reduce:[stroke-dasharray:none] [stroke-width:2px] [transition-duration:560ms]" data-chars="35" pathLength="1" vector-effect="non-scaling-stroke"/></svg>
<span class="relative z-[1] whitespace-nowrap transition-colors duration-[260ms] motion-reduce:transition-none group-hover:text-svgb20-stroke group-focus-visible:text-svgb20-stroke">Duplicate workspace and invite team</span>
</button>
</div>
</div>
</div>
</div>
</section>@import "tailwindcss";
/* Both properties must be registered, and neither can be a utility. An
unregistered custom property is an untyped token, so calc() with a unit can
fail to resolve — and an unregistered number cannot be interpolated, which
is what the hover transition depends on. */
@property --svgb-20-chars {
syntax: '<integer>';
inherits: true;
initial-value: 12;
}
/* the duty cycle of the measure: 0.65 = ticks with gaps, 1 = a closed border */
@property --svgb-20-duty {
syntax: '<number>';
inherits: true;
initial-value: 0.65;
}
@theme {
--color-svgb20-bg: #101010;
--color-svgb20-bg: oklch(0.18 0 0);
--color-svgb20-fg: #f4f4f0;
--color-svgb20-fg: oklch(0.96 0.005 100);
--color-svgb20-muted: #8d8d86;
--color-svgb20-muted: oklch(0.67 0.008 100);
--color-svgb20-rest: rgba(244, 244, 240, 0.16);
--color-svgb20-rest: oklch(0.96 0.005 100 / 0.16);
--color-svgb20-stroke: #d8ff2b;
--color-svgb20-stroke: oklch(0.93 0.22 116);
--font-svgb20-sans: 'Archivo', system-ui, sans-serif;
--font-svgb20-mono: 'JetBrains Mono', ui-monospace, monospace;
}
/* Progressive enhancement: attr() with a type reads the count straight off the
rect and drops the inline custom property entirely. A feature query is not a
variant, so this cannot move into the markup. */
@supports (stroke-width: attr(data-chars type(<number>))) {
.svgb-20__ink {
stroke-width: clamp(1.5px, calc(1.1px + attr(data-chars type(<number>), 12) * 0.055px), 3px);
}
}
.svgb-20:has(#svgb-20-theme:checked) {
--color-svgb20-bg: #f4f4f0;
--color-svgb20-bg: oklch(0.96 0.005 100);
--color-svgb20-fg: #111111;
--color-svgb20-fg: oklch(0.185 0 0);
--color-svgb20-muted: #5e5e58;
--color-svgb20-muted: oklch(0.50 0.008 100);
--color-svgb20-rest: rgba(17, 17, 17, 0.18);
--color-svgb20-rest: oklch(0.185 0 0 / 0.18);
--color-svgb20-stroke: #4a5c00;
--color-svgb20-stroke: oklch(0.44 0.14 116);
}@import "tailwindcss";
/* Both properties must be registered, and neither can be a utility. An
unregistered custom property is an untyped token, so calc() with a unit can
fail to resolve — and an unregistered number cannot be interpolated, which
is what the hover transition depends on. */
@property --svgb-20-chars {
syntax: '<integer>';
inherits: true;
initial-value: 12;
}
/* the duty cycle of the measure: 0.65 = ticks with gaps, 1 = a closed border */
@property --svgb-20-duty {
syntax: '<number>';
inherits: true;
initial-value: 0.65;
}
@theme {
--color-svgb20-bg: #101010;
--color-svgb20-bg: oklch(0.18 0 0);
--color-svgb20-fg: #f4f4f0;
--color-svgb20-fg: oklch(0.96 0.005 100);
--color-svgb20-muted: #8d8d86;
--color-svgb20-muted: oklch(0.67 0.008 100);
--color-svgb20-rest: rgba(244, 244, 240, 0.16);
--color-svgb20-rest: oklch(0.96 0.005 100 / 0.16);
--color-svgb20-stroke: #d8ff2b;
--color-svgb20-stroke: oklch(0.93 0.22 116);
--font-svgb20-sans: 'Archivo', system-ui, sans-serif;
--font-svgb20-mono: 'JetBrains Mono', ui-monospace, monospace;
}
/* Progressive enhancement: attr() with a type reads the count straight off the
rect and drops the inline custom property entirely. A feature query is not a
variant, so this cannot move into the markup. */
@supports (stroke-width: attr(data-chars type(<number>))) {
.svgb-20__ink {
stroke-width: clamp(1.5px, calc(1.1px + attr(data-chars type(<number>), 12) * 0.055px), 3px);
}
}
.svgb-20:has(#svgb-20-theme:checked) {
--color-svgb20-bg: #f4f4f0;
--color-svgb20-bg: oklch(0.96 0.005 100);
--color-svgb20-fg: #111111;
--color-svgb20-fg: oklch(0.185 0 0);
--color-svgb20-muted: #5e5e58;
--color-svgb20-muted: oklch(0.50 0.008 100);
--color-svgb20-rest: rgba(17, 17, 17, 0.18);
--color-svgb20-rest: oklch(0.185 0 0 / 0.18);
--color-svgb20-stroke: #4a5c00;
--color-svgb20-stroke: oklch(0.44 0.14 116);
}/* Optional convenience: the count can equally be printed into the markup by your template. */
(function () {
var root = document.querySelector('.svgb-20');
if (!root) return;
root.querySelectorAll('.svgb-20__btn').forEach(function (btn) {
var n = btn.querySelector('.svgb-20__label').textContent.trim().length;
btn.dataset.chars = String(n);
btn.style.setProperty('--svgb-20-chars', String(n));
/* attr() resolves against the element the declaration sits on, so the rect
needs its own copy — the button's attribute is not in scope for it. */
var ink = btn.querySelector('.svgb-20__ink');
if (ink) ink.dataset.chars = String(n);
});
})();/* Optional convenience: the count can equally be printed into the markup by your template. */
(function () {
var root = document.querySelector('.svgb-20');
if (!root) return;
root.querySelectorAll('.svgb-20__btn').forEach(function (btn) {
var n = btn.querySelector('.svgb-20__label').textContent.trim().length;
btn.dataset.chars = String(n);
btn.style.setProperty('--svgb-20-chars', String(n));
/* attr() resolves against the element the declaration sits on, so the rect
needs its own copy — the button's attribute is not in scope for it. */
var ink = btn.querySelector('.svgb-20__ink');
if (ink) ink.dataset.chars = String(n);
});
})();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";
/* Both properties must be registered, and neither can be a utility. An
unregistered custom property is an untyped token, so calc() with a unit can
fail to resolve — and an unregistered number cannot be interpolated, which
is what the hover transition depends on. */
@property --svgb-20-chars {
syntax: '<integer>';
inherits: true;
initial-value: 12;
}
/* the duty cycle of the measure: 0.65 = ticks with gaps, 1 = a closed border */
@property --svgb-20-duty {
syntax: '<number>';
inherits: true;
initial-value: 0.65;
}
@theme {
--color-svgb20-bg: #101010;
--color-svgb20-bg: oklch(0.18 0 0);
--color-svgb20-fg: #f4f4f0;
--color-svgb20-fg: oklch(0.96 0.005 100);
--color-svgb20-muted: #8d8d86;
--color-svgb20-muted: oklch(0.67 0.008 100);
--color-svgb20-rest: rgba(244, 244, 240, 0.16);
--color-svgb20-rest: oklch(0.96 0.005 100 / 0.16);
--color-svgb20-stroke: #d8ff2b;
--color-svgb20-stroke: oklch(0.93 0.22 116);
--font-svgb20-sans: 'Archivo', system-ui, sans-serif;
--font-svgb20-mono: 'JetBrains Mono', ui-monospace, monospace;
}
/* Progressive enhancement: attr() with a type reads the count straight off the
rect and drops the inline custom property entirely. A feature query is not a
variant, so this cannot move into the markup. */
@supports (stroke-width: attr(data-chars type(<number>))) {
.svgb-20__ink {
stroke-width: clamp(1.5px, calc(1.1px + attr(data-chars type(<number>), 12) * 0.055px), 3px);
}
}
.svgb-20:has(#svgb-20-theme:checked) {
--color-svgb20-bg: #f4f4f0;
--color-svgb20-bg: oklch(0.96 0.005 100);
--color-svgb20-fg: #111111;
--color-svgb20-fg: oklch(0.185 0 0);
--color-svgb20-muted: #5e5e58;
--color-svgb20-muted: oklch(0.50 0.008 100);
--color-svgb20-rest: rgba(17, 17, 17, 0.18);
--color-svgb20-rest: oklch(0.185 0 0 / 0.18);
--color-svgb20-stroke: #4a5c00;
--color-svgb20-stroke: oklch(0.44 0.14 116);
}@import "tailwindcss";
/* Both properties must be registered, and neither can be a utility. An
unregistered custom property is an untyped token, so calc() with a unit can
fail to resolve — and an unregistered number cannot be interpolated, which
is what the hover transition depends on. */
@property --svgb-20-chars {
syntax: '<integer>';
inherits: true;
initial-value: 12;
}
/* the duty cycle of the measure: 0.65 = ticks with gaps, 1 = a closed border */
@property --svgb-20-duty {
syntax: '<number>';
inherits: true;
initial-value: 0.65;
}
@theme {
--color-svgb20-bg: #101010;
--color-svgb20-bg: oklch(0.18 0 0);
--color-svgb20-fg: #f4f4f0;
--color-svgb20-fg: oklch(0.96 0.005 100);
--color-svgb20-muted: #8d8d86;
--color-svgb20-muted: oklch(0.67 0.008 100);
--color-svgb20-rest: rgba(244, 244, 240, 0.16);
--color-svgb20-rest: oklch(0.96 0.005 100 / 0.16);
--color-svgb20-stroke: #d8ff2b;
--color-svgb20-stroke: oklch(0.93 0.22 116);
--font-svgb20-sans: 'Archivo', system-ui, sans-serif;
--font-svgb20-mono: 'JetBrains Mono', ui-monospace, monospace;
}
/* Progressive enhancement: attr() with a type reads the count straight off the
rect and drops the inline custom property entirely. A feature query is not a
variant, so this cannot move into the markup. */
@supports (stroke-width: attr(data-chars type(<number>))) {
.svgb-20__ink {
stroke-width: clamp(1.5px, calc(1.1px + attr(data-chars type(<number>), 12) * 0.055px), 3px);
}
}
.svgb-20:has(#svgb-20-theme:checked) {
--color-svgb20-bg: #f4f4f0;
--color-svgb20-bg: oklch(0.96 0.005 100);
--color-svgb20-fg: #111111;
--color-svgb20-fg: oklch(0.185 0 0);
--color-svgb20-muted: #5e5e58;
--color-svgb20-muted: oklch(0.50 0.008 100);
--color-svgb20-rest: rgba(17, 17, 17, 0.18);
--color-svgb20-rest: oklch(0.185 0 0 / 0.18);
--color-svgb20-stroke: #4a5c00;
--color-svgb20-stroke: oklch(0.44 0.14 116);
}<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=Archivo:wdth,wght@75..125,400;75..125,700&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=Archivo:wdth,wght@75..125,400;75..125,700&family=JetBrains+Mono:wght@400;500&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
- The webfonts load as
<link>tags 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 character count arrives as an inline custom property —
style="--svgb-20-chars: 22"— and every derived value is an arbitrary utility reading it: stroke width, font size, dash pattern and the transition duration. - Hover transitions the dash pattern, not the offset:
hover:[--svgb-20-duty:1]on the button closes every gap until the measure becomes a solid border. That is a state change no dash-offset draw can express. - Both
@propertyblocks stay in CSS, and they are the load-bearing part: an unregistered--svgb-20-dutycannot be interpolated, so the gaps would snap shut instead of closing. - SVG geometry is set through arbitrary properties —
[x:1.5px] [width:calc(100%-3px)] [rx:4px]— which is what lets one rect fit six button widths with no viewBox at all. - The dash pattern divides by the count on a
pathLength="1"rect, socalc(var(--svgb-20-duty)/var(--svgb-20-chars))is exactly one tick per character at any width. - The
attr(data-chars type(<number>))enhancement also stays in CSS: a feature query has no variant form, and the declaration has to resolve on the rect that carries the attribute rather than on its button.
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 Variable Width Stroke Button with Text Length
Source: https://codefronts.com/components/tailwind-svg-stroke-buttons/tailwind-variable-width-stroke-button-with-text-length/
Six labels from three characters to thirty-five, each deriving its stroke weight, dash rhythm and draw duration from one inline number. Hover transitions the dash pattern rather than the offset — hover:[--svgb-20-duty:1] closes every gap until the measure becomes a solid border, a state change no dash-offset draw can express. Both @property blocks stay in CSS, and they are the load-bearing part.
## HTML
```html
<section class="svgb-20 relative block w-full min-h-svh bg-svgb20-bg text-svgb20-fg font-svgb20-sans">
<input class="peer absolute opacity-0 pointer-events-none" type="checkbox" id="svgb-20-theme">
<label class="absolute z-[5] top-4 right-4 grid place-items-center size-11 rounded-sm border border-current/22 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-20-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(22px,5vw,68px)]">
<div class="w-full max-w-225 flex flex-col gap-[clamp(20px,3vw,32px)]">
<div class="flex flex-wrap items-end justify-between gap-3.5 pb-4 border-b border-svgb20-rest">
<h1 class="m-0 text-[clamp(38px,7vw,82px)] font-bold [font-stretch:118%] leading-[0.88] tracking-[-0.045em] uppercase">Weight<br>Ladder</h1>
<p class="m-0 font-svgb20-mono text-[11px] tracking-[0.02em] text-svgb20-muted">stroke-width: clamp(1.5px, 1.1px + chars × 0.055px, 3px)</p>
</div>
<ol class="list-none m-0 p-0 flex flex-col gap-[clamp(12px,2vw,18px)]">
<li class="grid grid-cols-[34px_1fr_54px] gap-[clamp(12px,2.5vw,22px)] items-center">
<span class="font-svgb20-mono text-[11px] tabular-nums text-svgb20-muted">03</span>
<button class="group relative inline-grid place-items-center justify-self-start min-w-24 min-h-[62px] px-[clamp(20px,2.6vw,32px)] border-0 rounded bg-transparent font-normal tracking-[-0.01em] text-[clamp(15px,calc(14px+var(--svgb-20-chars)*0.04px),18px)] cursor-pointer [transition:--svgb-20-duty_calc(340ms+var(--svgb-20-chars)*14ms)_cubic-bezier(0.22,1,0.36,1)] hover:[--svgb-20-duty:1] focus-visible:[--svgb-20-duty:1] motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-svgb20-stroke focus-visible:outline-offset-[3px]" type="button" data-chars="3" style="--svgb-20-chars: 3">
<svg class="absolute inset-0 w-full h-full overflow-visible" aria-hidden="true"><rect class="svgb-20__ink fill-none [x:1.5px] [y:1.5px] [width:calc(100%-3px)] [height:calc(100%-3px)] [rx:4px] stroke-svgb20-rest [stroke-linecap:butt] [stroke-dasharray:calc(var(--svgb-20-duty)/var(--svgb-20-chars))_calc((1-var(--svgb-20-duty))/var(--svgb-20-chars))] transition-[stroke] duration-[240ms] group-hover:stroke-svgb20-stroke group-focus-visible:stroke-svgb20-stroke motion-reduce:[stroke-dasharray:none] [stroke-width:clamp(1.5px,calc(1.1px+var(--svgb-20-chars)*0.055px),3px)]" data-chars="3" pathLength="1" vector-effect="non-scaling-stroke"/></svg>
<span class="relative z-[1] whitespace-nowrap transition-colors duration-[260ms] motion-reduce:transition-none group-hover:text-svgb20-stroke group-focus-visible:text-svgb20-stroke">Buy</span>
</button>
<span class="font-svgb20-mono text-[11px] tabular-nums text-svgb20-muted text-end">1.5px</span>
</li>
<li class="grid grid-cols-[34px_1fr_54px] gap-[clamp(12px,2.5vw,22px)] items-center">
<span class="font-svgb20-mono text-[11px] tabular-nums text-svgb20-muted">08</span>
<button class="group relative inline-grid place-items-center justify-self-start min-w-24 min-h-[62px] px-[clamp(20px,2.6vw,32px)] border-0 rounded bg-transparent font-normal tracking-[-0.01em] text-[clamp(15px,calc(14px+var(--svgb-20-chars)*0.04px),18px)] cursor-pointer [transition:--svgb-20-duty_calc(340ms+var(--svgb-20-chars)*14ms)_cubic-bezier(0.22,1,0.36,1)] hover:[--svgb-20-duty:1] focus-visible:[--svgb-20-duty:1] motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-svgb20-stroke focus-visible:outline-offset-[3px]" type="button" data-chars="8" style="--svgb-20-chars: 8">
<svg class="absolute inset-0 w-full h-full overflow-visible" aria-hidden="true"><rect class="svgb-20__ink fill-none [x:1.5px] [y:1.5px] [width:calc(100%-3px)] [height:calc(100%-3px)] [rx:4px] stroke-svgb20-rest [stroke-linecap:butt] [stroke-dasharray:calc(var(--svgb-20-duty)/var(--svgb-20-chars))_calc((1-var(--svgb-20-duty))/var(--svgb-20-chars))] transition-[stroke] duration-[240ms] group-hover:stroke-svgb20-stroke group-focus-visible:stroke-svgb20-stroke motion-reduce:[stroke-dasharray:none] [stroke-width:clamp(1.5px,calc(1.1px+var(--svgb-20-chars)*0.055px),3px)]" data-chars="8" pathLength="1" vector-effect="non-scaling-stroke"/></svg>
<span class="relative z-[1] whitespace-nowrap transition-colors duration-[260ms] motion-reduce:transition-none group-hover:text-svgb20-stroke group-focus-visible:text-svgb20-stroke">Add note</span>
</button>
<span class="font-svgb20-mono text-[11px] tabular-nums text-svgb20-muted text-end">1.5px</span>
</li>
<li class="grid grid-cols-[34px_1fr_54px] gap-[clamp(12px,2.5vw,22px)] items-center">
<span class="font-svgb20-mono text-[11px] tabular-nums text-svgb20-muted">13</span>
<button class="group relative inline-grid place-items-center justify-self-start min-w-24 min-h-[62px] px-[clamp(20px,2.6vw,32px)] border-0 rounded bg-transparent font-normal tracking-[-0.01em] text-[clamp(15px,calc(14px+var(--svgb-20-chars)*0.04px),18px)] cursor-pointer [transition:--svgb-20-duty_calc(340ms+var(--svgb-20-chars)*14ms)_cubic-bezier(0.22,1,0.36,1)] hover:[--svgb-20-duty:1] focus-visible:[--svgb-20-duty:1] motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-svgb20-stroke focus-visible:outline-offset-[3px]" type="button" data-chars="13" style="--svgb-20-chars: 13">
<svg class="absolute inset-0 w-full h-full overflow-visible" aria-hidden="true"><rect class="svgb-20__ink fill-none [x:1.5px] [y:1.5px] [width:calc(100%-3px)] [height:calc(100%-3px)] [rx:4px] stroke-svgb20-rest [stroke-linecap:butt] [stroke-dasharray:calc(var(--svgb-20-duty)/var(--svgb-20-chars))_calc((1-var(--svgb-20-duty))/var(--svgb-20-chars))] transition-[stroke] duration-[240ms] group-hover:stroke-svgb20-stroke group-focus-visible:stroke-svgb20-stroke motion-reduce:[stroke-dasharray:none] [stroke-width:clamp(1.5px,calc(1.1px+var(--svgb-20-chars)*0.055px),3px)]" data-chars="13" pathLength="1" vector-effect="non-scaling-stroke"/></svg>
<span class="relative z-[1] whitespace-nowrap transition-colors duration-[260ms] motion-reduce:transition-none group-hover:text-svgb20-stroke group-focus-visible:text-svgb20-stroke">Export as CSV</span>
</button>
<span class="font-svgb20-mono text-[11px] tabular-nums text-svgb20-muted text-end">1.8px</span>
</li>
<li class="grid grid-cols-[34px_1fr_54px] gap-[clamp(12px,2.5vw,22px)] items-center">
<span class="font-svgb20-mono text-[11px] tabular-nums text-svgb20-muted">22</span>
<button class="group relative inline-grid place-items-center justify-self-start min-w-24 min-h-[62px] px-[clamp(20px,2.6vw,32px)] border-0 rounded bg-transparent font-normal tracking-[-0.01em] text-[clamp(15px,calc(14px+var(--svgb-20-chars)*0.04px),18px)] cursor-pointer [transition:--svgb-20-duty_calc(340ms+var(--svgb-20-chars)*14ms)_cubic-bezier(0.22,1,0.36,1)] hover:[--svgb-20-duty:1] focus-visible:[--svgb-20-duty:1] motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-svgb20-stroke focus-visible:outline-offset-[3px]" type="button" data-chars="22" style="--svgb-20-chars: 22">
<svg class="absolute inset-0 w-full h-full overflow-visible" aria-hidden="true"><rect class="svgb-20__ink fill-none [x:1.5px] [y:1.5px] [width:calc(100%-3px)] [height:calc(100%-3px)] [rx:4px] stroke-svgb20-rest [stroke-linecap:butt] [stroke-dasharray:calc(var(--svgb-20-duty)/var(--svgb-20-chars))_calc((1-var(--svgb-20-duty))/var(--svgb-20-chars))] transition-[stroke] duration-[240ms] group-hover:stroke-svgb20-stroke group-focus-visible:stroke-svgb20-stroke motion-reduce:[stroke-dasharray:none] [stroke-width:clamp(1.5px,calc(1.1px+var(--svgb-20-chars)*0.055px),3px)]" data-chars="22" pathLength="1" vector-effect="non-scaling-stroke"/></svg>
<span class="relative z-[1] whitespace-nowrap transition-colors duration-[260ms] motion-reduce:transition-none group-hover:text-svgb20-stroke group-focus-visible:text-svgb20-stroke">Schedule for next week</span>
</button>
<span class="font-svgb20-mono text-[11px] tabular-nums text-svgb20-muted text-end">2.3px</span>
</li>
<li class="grid grid-cols-[34px_1fr_54px] gap-[clamp(12px,2.5vw,22px)] items-center">
<span class="font-svgb20-mono text-[11px] tabular-nums text-svgb20-muted">35</span>
<button class="group relative inline-grid place-items-center justify-self-start min-w-24 min-h-[62px] px-[clamp(20px,2.6vw,32px)] border-0 rounded bg-transparent font-normal tracking-[-0.01em] text-[clamp(15px,calc(14px+var(--svgb-20-chars)*0.04px),18px)] cursor-pointer [transition:--svgb-20-duty_calc(340ms+var(--svgb-20-chars)*14ms)_cubic-bezier(0.22,1,0.36,1)] hover:[--svgb-20-duty:1] focus-visible:[--svgb-20-duty:1] motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-svgb20-stroke focus-visible:outline-offset-[3px]" type="button" data-chars="35" style="--svgb-20-chars: 35">
<svg class="absolute inset-0 w-full h-full overflow-visible" aria-hidden="true"><rect class="svgb-20__ink fill-none [x:1.5px] [y:1.5px] [width:calc(100%-3px)] [height:calc(100%-3px)] [rx:4px] stroke-svgb20-rest [stroke-linecap:butt] [stroke-dasharray:calc(var(--svgb-20-duty)/var(--svgb-20-chars))_calc((1-var(--svgb-20-duty))/var(--svgb-20-chars))] transition-[stroke] duration-[240ms] group-hover:stroke-svgb20-stroke group-focus-visible:stroke-svgb20-stroke motion-reduce:[stroke-dasharray:none] [stroke-width:clamp(1.5px,calc(1.1px+var(--svgb-20-chars)*0.055px),3px)]" data-chars="35" pathLength="1" vector-effect="non-scaling-stroke"/></svg>
<span class="relative z-[1] whitespace-nowrap transition-colors duration-[260ms] motion-reduce:transition-none group-hover:text-svgb20-stroke group-focus-visible:text-svgb20-stroke">Duplicate workspace and invite team</span>
</button>
<span class="font-svgb20-mono text-[11px] tabular-nums text-svgb20-muted text-end">3.0px</span>
</li>
</ol>
<div class="flex flex-col gap-3 pt-[18px] border-t border-svgb20-rest">
<span class="font-svgb20-mono text-[10.5px] tracking-[0.16em] uppercase text-svgb20-muted">control · fixed 2px</span>
<div class="flex flex-wrap items-center gap-3.5">
<button class="group relative inline-grid place-items-center justify-self-start min-w-24 min-h-[62px] px-[clamp(20px,2.6vw,32px)] border-0 rounded bg-transparent font-normal tracking-[-0.01em] text-[clamp(15px,calc(14px+var(--svgb-20-chars)*0.04px),18px)] cursor-pointer [transition:--svgb-20-duty_calc(340ms+var(--svgb-20-chars)*14ms)_cubic-bezier(0.22,1,0.36,1)] hover:[--svgb-20-duty:1] focus-visible:[--svgb-20-duty:1] motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-svgb20-stroke focus-visible:outline-offset-[3px] text-svgb20-muted" type="button" data-chars="3" style="--svgb-20-chars: 3">
<svg class="absolute inset-0 w-full h-full overflow-visible" aria-hidden="true"><rect class="svgb-20__ink fill-none [x:1.5px] [y:1.5px] [width:calc(100%-3px)] [height:calc(100%-3px)] [rx:4px] stroke-svgb20-rest [stroke-linecap:butt] [stroke-dasharray:calc(var(--svgb-20-duty)/var(--svgb-20-chars))_calc((1-var(--svgb-20-duty))/var(--svgb-20-chars))] transition-[stroke] duration-[240ms] group-hover:stroke-svgb20-stroke group-focus-visible:stroke-svgb20-stroke motion-reduce:[stroke-dasharray:none] [stroke-width:2px] [transition-duration:560ms]" data-chars="3" pathLength="1" vector-effect="non-scaling-stroke"/></svg>
<span class="relative z-[1] whitespace-nowrap transition-colors duration-[260ms] motion-reduce:transition-none group-hover:text-svgb20-stroke group-focus-visible:text-svgb20-stroke">Buy</span>
</button>
<button class="group relative inline-grid place-items-center justify-self-start min-w-24 min-h-[62px] px-[clamp(20px,2.6vw,32px)] border-0 rounded bg-transparent font-normal tracking-[-0.01em] text-[clamp(15px,calc(14px+var(--svgb-20-chars)*0.04px),18px)] cursor-pointer [transition:--svgb-20-duty_calc(340ms+var(--svgb-20-chars)*14ms)_cubic-bezier(0.22,1,0.36,1)] hover:[--svgb-20-duty:1] focus-visible:[--svgb-20-duty:1] motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-svgb20-stroke focus-visible:outline-offset-[3px] text-svgb20-muted" type="button" data-chars="35" style="--svgb-20-chars: 35">
<svg class="absolute inset-0 w-full h-full overflow-visible" aria-hidden="true"><rect class="svgb-20__ink fill-none [x:1.5px] [y:1.5px] [width:calc(100%-3px)] [height:calc(100%-3px)] [rx:4px] stroke-svgb20-rest [stroke-linecap:butt] [stroke-dasharray:calc(var(--svgb-20-duty)/var(--svgb-20-chars))_calc((1-var(--svgb-20-duty))/var(--svgb-20-chars))] transition-[stroke] duration-[240ms] group-hover:stroke-svgb20-stroke group-focus-visible:stroke-svgb20-stroke motion-reduce:[stroke-dasharray:none] [stroke-width:2px] [transition-duration:560ms]" data-chars="35" pathLength="1" vector-effect="non-scaling-stroke"/></svg>
<span class="relative z-[1] whitespace-nowrap transition-colors duration-[260ms] motion-reduce:transition-none group-hover:text-svgb20-stroke group-focus-visible:text-svgb20-stroke">Duplicate workspace and invite team</span>
</button>
</div>
</div>
</div>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
/* Both properties must be registered, and neither can be a utility. An
unregistered custom property is an untyped token, so calc() with a unit can
fail to resolve — and an unregistered number cannot be interpolated, which
is what the hover transition depends on. */
@property --svgb-20-chars {
syntax: '<integer>';
inherits: true;
initial-value: 12;
}
/* the duty cycle of the measure: 0.65 = ticks with gaps, 1 = a closed border */
@property --svgb-20-duty {
syntax: '<number>';
inherits: true;
initial-value: 0.65;
}
@theme {
--color-svgb20-bg: #101010;
--color-svgb20-bg: oklch(0.18 0 0);
--color-svgb20-fg: #f4f4f0;
--color-svgb20-fg: oklch(0.96 0.005 100);
--color-svgb20-muted: #8d8d86;
--color-svgb20-muted: oklch(0.67 0.008 100);
--color-svgb20-rest: rgba(244, 244, 240, 0.16);
--color-svgb20-rest: oklch(0.96 0.005 100 / 0.16);
--color-svgb20-stroke: #d8ff2b;
--color-svgb20-stroke: oklch(0.93 0.22 116);
--font-svgb20-sans: 'Archivo', system-ui, sans-serif;
--font-svgb20-mono: 'JetBrains Mono', ui-monospace, monospace;
}
/* Progressive enhancement: attr() with a type reads the count straight off the
rect and drops the inline custom property entirely. A feature query is not a
variant, so this cannot move into the markup. */
@supports (stroke-width: attr(data-chars type(<number>))) {
.svgb-20__ink {
stroke-width: clamp(1.5px, calc(1.1px + attr(data-chars type(<number>), 12) * 0.055px), 3px);
}
}
.svgb-20:has(#svgb-20-theme:checked) {
--color-svgb20-bg: #f4f4f0;
--color-svgb20-bg: oklch(0.96 0.005 100);
--color-svgb20-fg: #111111;
--color-svgb20-fg: oklch(0.185 0 0);
--color-svgb20-muted: #5e5e58;
--color-svgb20-muted: oklch(0.50 0.008 100);
--color-svgb20-rest: rgba(17, 17, 17, 0.18);
--color-svgb20-rest: oklch(0.185 0 0 / 0.18);
--color-svgb20-stroke: #4a5c00;
--color-svgb20-stroke: oklch(0.44 0.14 116);
}
```
## JavaScript
```js
/* Optional convenience: the count can equally be printed into the markup by your template. */
(function () {
var root = document.querySelector('.svgb-20');
if (!root) return;
root.querySelectorAll('.svgb-20__btn').forEach(function (btn) {
var n = btn.querySelector('.svgb-20__label').textContent.trim().length;
btn.dataset.chars = String(n);
btn.style.setProperty('--svgb-20-chars', String(n));
/* attr() resolves against the element the declaration sits on, so the rect
needs its own copy — the button's attribute is not in scope for it. */
var ink = btn.querySelector('.svgb-20__ink');
if (ink) ink.dataset.chars = String(n);
});
})();
```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 Variable Width Stroke Button with Text Length
Source: https://codefronts.com/components/tailwind-svg-stroke-buttons/tailwind-variable-width-stroke-button-with-text-length/
Six labels from three characters to thirty-five, each deriving its stroke weight, dash rhythm and draw duration from one inline number. Hover transitions the dash pattern rather than the offset — hover:[--svgb-20-duty:1] closes every gap until the measure becomes a solid border, a state change no dash-offset draw can express. Both @property blocks stay in CSS, and they are the load-bearing part.
## HTML
```html
<section class="svgb-20 relative block w-full min-h-svh bg-svgb20-bg text-svgb20-fg font-svgb20-sans">
<input class="peer absolute opacity-0 pointer-events-none" type="checkbox" id="svgb-20-theme">
<label class="absolute z-[5] top-4 right-4 grid place-items-center size-11 rounded-sm border border-current/22 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-20-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(22px,5vw,68px)]">
<div class="w-full max-w-225 flex flex-col gap-[clamp(20px,3vw,32px)]">
<div class="flex flex-wrap items-end justify-between gap-3.5 pb-4 border-b border-svgb20-rest">
<h1 class="m-0 text-[clamp(38px,7vw,82px)] font-bold [font-stretch:118%] leading-[0.88] tracking-[-0.045em] uppercase">Weight<br>Ladder</h1>
<p class="m-0 font-svgb20-mono text-[11px] tracking-[0.02em] text-svgb20-muted">stroke-width: clamp(1.5px, 1.1px + chars × 0.055px, 3px)</p>
</div>
<ol class="list-none m-0 p-0 flex flex-col gap-[clamp(12px,2vw,18px)]">
<li class="grid grid-cols-[34px_1fr_54px] gap-[clamp(12px,2.5vw,22px)] items-center">
<span class="font-svgb20-mono text-[11px] tabular-nums text-svgb20-muted">03</span>
<button class="group relative inline-grid place-items-center justify-self-start min-w-24 min-h-[62px] px-[clamp(20px,2.6vw,32px)] border-0 rounded bg-transparent font-normal tracking-[-0.01em] text-[clamp(15px,calc(14px+var(--svgb-20-chars)*0.04px),18px)] cursor-pointer [transition:--svgb-20-duty_calc(340ms+var(--svgb-20-chars)*14ms)_cubic-bezier(0.22,1,0.36,1)] hover:[--svgb-20-duty:1] focus-visible:[--svgb-20-duty:1] motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-svgb20-stroke focus-visible:outline-offset-[3px]" type="button" data-chars="3" style="--svgb-20-chars: 3">
<svg class="absolute inset-0 w-full h-full overflow-visible" aria-hidden="true"><rect class="svgb-20__ink fill-none [x:1.5px] [y:1.5px] [width:calc(100%-3px)] [height:calc(100%-3px)] [rx:4px] stroke-svgb20-rest [stroke-linecap:butt] [stroke-dasharray:calc(var(--svgb-20-duty)/var(--svgb-20-chars))_calc((1-var(--svgb-20-duty))/var(--svgb-20-chars))] transition-[stroke] duration-[240ms] group-hover:stroke-svgb20-stroke group-focus-visible:stroke-svgb20-stroke motion-reduce:[stroke-dasharray:none] [stroke-width:clamp(1.5px,calc(1.1px+var(--svgb-20-chars)*0.055px),3px)]" data-chars="3" pathLength="1" vector-effect="non-scaling-stroke"/></svg>
<span class="relative z-[1] whitespace-nowrap transition-colors duration-[260ms] motion-reduce:transition-none group-hover:text-svgb20-stroke group-focus-visible:text-svgb20-stroke">Buy</span>
</button>
<span class="font-svgb20-mono text-[11px] tabular-nums text-svgb20-muted text-end">1.5px</span>
</li>
<li class="grid grid-cols-[34px_1fr_54px] gap-[clamp(12px,2.5vw,22px)] items-center">
<span class="font-svgb20-mono text-[11px] tabular-nums text-svgb20-muted">08</span>
<button class="group relative inline-grid place-items-center justify-self-start min-w-24 min-h-[62px] px-[clamp(20px,2.6vw,32px)] border-0 rounded bg-transparent font-normal tracking-[-0.01em] text-[clamp(15px,calc(14px+var(--svgb-20-chars)*0.04px),18px)] cursor-pointer [transition:--svgb-20-duty_calc(340ms+var(--svgb-20-chars)*14ms)_cubic-bezier(0.22,1,0.36,1)] hover:[--svgb-20-duty:1] focus-visible:[--svgb-20-duty:1] motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-svgb20-stroke focus-visible:outline-offset-[3px]" type="button" data-chars="8" style="--svgb-20-chars: 8">
<svg class="absolute inset-0 w-full h-full overflow-visible" aria-hidden="true"><rect class="svgb-20__ink fill-none [x:1.5px] [y:1.5px] [width:calc(100%-3px)] [height:calc(100%-3px)] [rx:4px] stroke-svgb20-rest [stroke-linecap:butt] [stroke-dasharray:calc(var(--svgb-20-duty)/var(--svgb-20-chars))_calc((1-var(--svgb-20-duty))/var(--svgb-20-chars))] transition-[stroke] duration-[240ms] group-hover:stroke-svgb20-stroke group-focus-visible:stroke-svgb20-stroke motion-reduce:[stroke-dasharray:none] [stroke-width:clamp(1.5px,calc(1.1px+var(--svgb-20-chars)*0.055px),3px)]" data-chars="8" pathLength="1" vector-effect="non-scaling-stroke"/></svg>
<span class="relative z-[1] whitespace-nowrap transition-colors duration-[260ms] motion-reduce:transition-none group-hover:text-svgb20-stroke group-focus-visible:text-svgb20-stroke">Add note</span>
</button>
<span class="font-svgb20-mono text-[11px] tabular-nums text-svgb20-muted text-end">1.5px</span>
</li>
<li class="grid grid-cols-[34px_1fr_54px] gap-[clamp(12px,2.5vw,22px)] items-center">
<span class="font-svgb20-mono text-[11px] tabular-nums text-svgb20-muted">13</span>
<button class="group relative inline-grid place-items-center justify-self-start min-w-24 min-h-[62px] px-[clamp(20px,2.6vw,32px)] border-0 rounded bg-transparent font-normal tracking-[-0.01em] text-[clamp(15px,calc(14px+var(--svgb-20-chars)*0.04px),18px)] cursor-pointer [transition:--svgb-20-duty_calc(340ms+var(--svgb-20-chars)*14ms)_cubic-bezier(0.22,1,0.36,1)] hover:[--svgb-20-duty:1] focus-visible:[--svgb-20-duty:1] motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-svgb20-stroke focus-visible:outline-offset-[3px]" type="button" data-chars="13" style="--svgb-20-chars: 13">
<svg class="absolute inset-0 w-full h-full overflow-visible" aria-hidden="true"><rect class="svgb-20__ink fill-none [x:1.5px] [y:1.5px] [width:calc(100%-3px)] [height:calc(100%-3px)] [rx:4px] stroke-svgb20-rest [stroke-linecap:butt] [stroke-dasharray:calc(var(--svgb-20-duty)/var(--svgb-20-chars))_calc((1-var(--svgb-20-duty))/var(--svgb-20-chars))] transition-[stroke] duration-[240ms] group-hover:stroke-svgb20-stroke group-focus-visible:stroke-svgb20-stroke motion-reduce:[stroke-dasharray:none] [stroke-width:clamp(1.5px,calc(1.1px+var(--svgb-20-chars)*0.055px),3px)]" data-chars="13" pathLength="1" vector-effect="non-scaling-stroke"/></svg>
<span class="relative z-[1] whitespace-nowrap transition-colors duration-[260ms] motion-reduce:transition-none group-hover:text-svgb20-stroke group-focus-visible:text-svgb20-stroke">Export as CSV</span>
</button>
<span class="font-svgb20-mono text-[11px] tabular-nums text-svgb20-muted text-end">1.8px</span>
</li>
<li class="grid grid-cols-[34px_1fr_54px] gap-[clamp(12px,2.5vw,22px)] items-center">
<span class="font-svgb20-mono text-[11px] tabular-nums text-svgb20-muted">22</span>
<button class="group relative inline-grid place-items-center justify-self-start min-w-24 min-h-[62px] px-[clamp(20px,2.6vw,32px)] border-0 rounded bg-transparent font-normal tracking-[-0.01em] text-[clamp(15px,calc(14px+var(--svgb-20-chars)*0.04px),18px)] cursor-pointer [transition:--svgb-20-duty_calc(340ms+var(--svgb-20-chars)*14ms)_cubic-bezier(0.22,1,0.36,1)] hover:[--svgb-20-duty:1] focus-visible:[--svgb-20-duty:1] motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-svgb20-stroke focus-visible:outline-offset-[3px]" type="button" data-chars="22" style="--svgb-20-chars: 22">
<svg class="absolute inset-0 w-full h-full overflow-visible" aria-hidden="true"><rect class="svgb-20__ink fill-none [x:1.5px] [y:1.5px] [width:calc(100%-3px)] [height:calc(100%-3px)] [rx:4px] stroke-svgb20-rest [stroke-linecap:butt] [stroke-dasharray:calc(var(--svgb-20-duty)/var(--svgb-20-chars))_calc((1-var(--svgb-20-duty))/var(--svgb-20-chars))] transition-[stroke] duration-[240ms] group-hover:stroke-svgb20-stroke group-focus-visible:stroke-svgb20-stroke motion-reduce:[stroke-dasharray:none] [stroke-width:clamp(1.5px,calc(1.1px+var(--svgb-20-chars)*0.055px),3px)]" data-chars="22" pathLength="1" vector-effect="non-scaling-stroke"/></svg>
<span class="relative z-[1] whitespace-nowrap transition-colors duration-[260ms] motion-reduce:transition-none group-hover:text-svgb20-stroke group-focus-visible:text-svgb20-stroke">Schedule for next week</span>
</button>
<span class="font-svgb20-mono text-[11px] tabular-nums text-svgb20-muted text-end">2.3px</span>
</li>
<li class="grid grid-cols-[34px_1fr_54px] gap-[clamp(12px,2.5vw,22px)] items-center">
<span class="font-svgb20-mono text-[11px] tabular-nums text-svgb20-muted">35</span>
<button class="group relative inline-grid place-items-center justify-self-start min-w-24 min-h-[62px] px-[clamp(20px,2.6vw,32px)] border-0 rounded bg-transparent font-normal tracking-[-0.01em] text-[clamp(15px,calc(14px+var(--svgb-20-chars)*0.04px),18px)] cursor-pointer [transition:--svgb-20-duty_calc(340ms+var(--svgb-20-chars)*14ms)_cubic-bezier(0.22,1,0.36,1)] hover:[--svgb-20-duty:1] focus-visible:[--svgb-20-duty:1] motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-svgb20-stroke focus-visible:outline-offset-[3px]" type="button" data-chars="35" style="--svgb-20-chars: 35">
<svg class="absolute inset-0 w-full h-full overflow-visible" aria-hidden="true"><rect class="svgb-20__ink fill-none [x:1.5px] [y:1.5px] [width:calc(100%-3px)] [height:calc(100%-3px)] [rx:4px] stroke-svgb20-rest [stroke-linecap:butt] [stroke-dasharray:calc(var(--svgb-20-duty)/var(--svgb-20-chars))_calc((1-var(--svgb-20-duty))/var(--svgb-20-chars))] transition-[stroke] duration-[240ms] group-hover:stroke-svgb20-stroke group-focus-visible:stroke-svgb20-stroke motion-reduce:[stroke-dasharray:none] [stroke-width:clamp(1.5px,calc(1.1px+var(--svgb-20-chars)*0.055px),3px)]" data-chars="35" pathLength="1" vector-effect="non-scaling-stroke"/></svg>
<span class="relative z-[1] whitespace-nowrap transition-colors duration-[260ms] motion-reduce:transition-none group-hover:text-svgb20-stroke group-focus-visible:text-svgb20-stroke">Duplicate workspace and invite team</span>
</button>
<span class="font-svgb20-mono text-[11px] tabular-nums text-svgb20-muted text-end">3.0px</span>
</li>
</ol>
<div class="flex flex-col gap-3 pt-[18px] border-t border-svgb20-rest">
<span class="font-svgb20-mono text-[10.5px] tracking-[0.16em] uppercase text-svgb20-muted">control · fixed 2px</span>
<div class="flex flex-wrap items-center gap-3.5">
<button class="group relative inline-grid place-items-center justify-self-start min-w-24 min-h-[62px] px-[clamp(20px,2.6vw,32px)] border-0 rounded bg-transparent font-normal tracking-[-0.01em] text-[clamp(15px,calc(14px+var(--svgb-20-chars)*0.04px),18px)] cursor-pointer [transition:--svgb-20-duty_calc(340ms+var(--svgb-20-chars)*14ms)_cubic-bezier(0.22,1,0.36,1)] hover:[--svgb-20-duty:1] focus-visible:[--svgb-20-duty:1] motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-svgb20-stroke focus-visible:outline-offset-[3px] text-svgb20-muted" type="button" data-chars="3" style="--svgb-20-chars: 3">
<svg class="absolute inset-0 w-full h-full overflow-visible" aria-hidden="true"><rect class="svgb-20__ink fill-none [x:1.5px] [y:1.5px] [width:calc(100%-3px)] [height:calc(100%-3px)] [rx:4px] stroke-svgb20-rest [stroke-linecap:butt] [stroke-dasharray:calc(var(--svgb-20-duty)/var(--svgb-20-chars))_calc((1-var(--svgb-20-duty))/var(--svgb-20-chars))] transition-[stroke] duration-[240ms] group-hover:stroke-svgb20-stroke group-focus-visible:stroke-svgb20-stroke motion-reduce:[stroke-dasharray:none] [stroke-width:2px] [transition-duration:560ms]" data-chars="3" pathLength="1" vector-effect="non-scaling-stroke"/></svg>
<span class="relative z-[1] whitespace-nowrap transition-colors duration-[260ms] motion-reduce:transition-none group-hover:text-svgb20-stroke group-focus-visible:text-svgb20-stroke">Buy</span>
</button>
<button class="group relative inline-grid place-items-center justify-self-start min-w-24 min-h-[62px] px-[clamp(20px,2.6vw,32px)] border-0 rounded bg-transparent font-normal tracking-[-0.01em] text-[clamp(15px,calc(14px+var(--svgb-20-chars)*0.04px),18px)] cursor-pointer [transition:--svgb-20-duty_calc(340ms+var(--svgb-20-chars)*14ms)_cubic-bezier(0.22,1,0.36,1)] hover:[--svgb-20-duty:1] focus-visible:[--svgb-20-duty:1] motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-svgb20-stroke focus-visible:outline-offset-[3px] text-svgb20-muted" type="button" data-chars="35" style="--svgb-20-chars: 35">
<svg class="absolute inset-0 w-full h-full overflow-visible" aria-hidden="true"><rect class="svgb-20__ink fill-none [x:1.5px] [y:1.5px] [width:calc(100%-3px)] [height:calc(100%-3px)] [rx:4px] stroke-svgb20-rest [stroke-linecap:butt] [stroke-dasharray:calc(var(--svgb-20-duty)/var(--svgb-20-chars))_calc((1-var(--svgb-20-duty))/var(--svgb-20-chars))] transition-[stroke] duration-[240ms] group-hover:stroke-svgb20-stroke group-focus-visible:stroke-svgb20-stroke motion-reduce:[stroke-dasharray:none] [stroke-width:2px] [transition-duration:560ms]" data-chars="35" pathLength="1" vector-effect="non-scaling-stroke"/></svg>
<span class="relative z-[1] whitespace-nowrap transition-colors duration-[260ms] motion-reduce:transition-none group-hover:text-svgb20-stroke group-focus-visible:text-svgb20-stroke">Duplicate workspace and invite team</span>
</button>
</div>
</div>
</div>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
/* Both properties must be registered, and neither can be a utility. An
unregistered custom property is an untyped token, so calc() with a unit can
fail to resolve — and an unregistered number cannot be interpolated, which
is what the hover transition depends on. */
@property --svgb-20-chars {
syntax: '<integer>';
inherits: true;
initial-value: 12;
}
/* the duty cycle of the measure: 0.65 = ticks with gaps, 1 = a closed border */
@property --svgb-20-duty {
syntax: '<number>';
inherits: true;
initial-value: 0.65;
}
@theme {
--color-svgb20-bg: #101010;
--color-svgb20-bg: oklch(0.18 0 0);
--color-svgb20-fg: #f4f4f0;
--color-svgb20-fg: oklch(0.96 0.005 100);
--color-svgb20-muted: #8d8d86;
--color-svgb20-muted: oklch(0.67 0.008 100);
--color-svgb20-rest: rgba(244, 244, 240, 0.16);
--color-svgb20-rest: oklch(0.96 0.005 100 / 0.16);
--color-svgb20-stroke: #d8ff2b;
--color-svgb20-stroke: oklch(0.93 0.22 116);
--font-svgb20-sans: 'Archivo', system-ui, sans-serif;
--font-svgb20-mono: 'JetBrains Mono', ui-monospace, monospace;
}
/* Progressive enhancement: attr() with a type reads the count straight off the
rect and drops the inline custom property entirely. A feature query is not a
variant, so this cannot move into the markup. */
@supports (stroke-width: attr(data-chars type(<number>))) {
.svgb-20__ink {
stroke-width: clamp(1.5px, calc(1.1px + attr(data-chars type(<number>), 12) * 0.055px), 3px);
}
}
.svgb-20:has(#svgb-20-theme:checked) {
--color-svgb20-bg: #f4f4f0;
--color-svgb20-bg: oklch(0.96 0.005 100);
--color-svgb20-fg: #111111;
--color-svgb20-fg: oklch(0.185 0 0);
--color-svgb20-muted: #5e5e58;
--color-svgb20-muted: oklch(0.50 0.008 100);
--color-svgb20-rest: rgba(17, 17, 17, 0.18);
--color-svgb20-rest: oklch(0.185 0 0 / 0.18);
--color-svgb20-stroke: #4a5c00;
--color-svgb20-stroke: oklch(0.44 0.14 116);
}
```
## JavaScript
```js
/* Optional convenience: the count can equally be printed into the markup by your template. */
(function () {
var root = document.querySelector('.svgb-20');
if (!root) return;
root.querySelectorAll('.svgb-20__btn').forEach(function (btn) {
var n = btn.querySelector('.svgb-20__label').textContent.trim().length;
btn.dataset.chars = String(n);
btn.style.setProperty('--svgb-20-chars', String(n));
/* attr() resolves against the element the declaration sits on, so the rect
needs its own copy — the button's attribute is not in scope for it. */
var ink = btn.querySelector('.svgb-20__ink');
if (ink) ink.dataset.chars = String(n);
});
})();
```How this works
One number, many consequences. The label's character count arrives inline and everything else is arithmetic on it:
<button style="--svgb-20-chars: 22"
class="[--svgb-20-duty:0.55]
hover:[--svgb-20-duty:1]">class="[stroke-dasharray:calc(var(--svgb-20-duty)/var(--svgb-20-chars))_calc((1-var(--svgb-20-duty))/var(--svgb-20-chars))]"Because the rect carries pathLength="1", dividing by the count gives exactly one tick per character — a 3-character label gets three coarse ticks, a 35-character label gets thirty-five fine ones, and the optical weight stays even.
Transitioning the pattern is a different idea. Every other demo here moves the offset. This one animates the duty cycle: at rest the ticks have gaps, on hover the duty goes to 1 and the measure becomes a solid border. There is no way to express “the gaps close” as a dash-offset animation.
Which is why @property is load-bearing. An unregistered custom property is a string to the interpolation engine, so --svgb-20-duty would jump from 0.55 to 1 in one frame. Registered as a <number>, it tweens — and the gaps visibly close. This cannot be a utility, and dropping it silently breaks the only effect the demo has.
@property --svgb-20-duty {
syntax: '<number>'; inherits: true; initial-value: 0.55;
}No viewBox at all. The rect is positioned with arbitrary geometry properties and a percentage width, so it fits whatever the label makes the button. That is what keeps six very different widths on one class string, and it is why nothing here needs measuring.
Make it yours
- Change the resting duty cycle to make the measure denser or airier; 0.55 reads as a ruler, 0.85 as a nearly solid line.
- Retime the close from the count-derived duration if long labels feel slow — the transition is deliberately proportional.
- Drop the control pair at the bottom once the point is made; they exist to show the problem, not to be UI.
- Feed the count from your template rather than the script — the script is a convenience, not a dependency.
- Scale the stroke-width expression if your type is much heavier than this specimen; the optical logic holds, the constants do not.
Gotchas — read before shipping
- Without
@propertythe duty cycle cannot interpolate and the gaps snap shut. This is the one demo where a registration is the effect rather than a nicety. calc()inside arbitrary values must have no spaces, which makes these class strings long and unforgiving — copy them whole.pathLength="1"is required for the per-character division to mean anything.- A percentage
widthon an SVG rect resolves against the SVG's own width, so the demo works only because there is no viewBox scaling in between. attr()with a type is very new; the enhancement is guarded and the inline variable remains the reliable path.- Deriving stroke weight from character count is an optical correction, not a rule — check it against your actual type, because a heavy display face needs different constants.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
@property sets the real floor (Chrome 85, Safari 16.4, Firefox 128) and is what makes the duty cycle animate. The attr(data-chars type(<number>)) enhancement is Chrome-only for now and guarded by a feature query, so the inline custom property is what actually runs everywhere.