10 / 20
All 20 in 20 CSS SVG Stroke Buttons
- 01SVG Stroke Draw Button Border on Hover
- 02Stroke Dashoffset Button Outline Animation
- 03Corner Start Perimeter Draw Button
- 04Two Path Split Border Draw Button
- 05Rounded Rect Stroke Button with rx Radius
- 06Dashed Marching Ants Button Border
- 07Gradient Stroke Button with SVG linearGradient
- 08Stroke Draw Button That Reverses on Mouse Leave
- 09Focus Visible Stroke Outline for Keyboard Users
- 10Non Scaling Stroke Button That Resizes with Text
- 11Stroke Draw Button with pathLength Normalization
- 12Dual Layer Stroke Button with Offset Trailing Path
- 13Sequential Stroke and Label Fade Button
- 14Icon Plus Border Stroke Draw Together
- 15Stroke Draw Submit Button Loading State
- 16Stroke Draw Button Disabled and Error States
- 17Scroll Triggered Button Stroke with animation-timeline
- 18Stroke Draw Button Group with Staggered Delay
- 19Reduced Motion Fallback for Stroke Buttons
- 20Variable Width Stroke Button with Text Length
Non Scaling Stroke Button That Resizes with Text
The same call to action in six languages, right-to-left included, each button sized by its own text. Every frame is one fixed 200×56 viewBox stretched to whatever width the label needs, so the resting border is genuinely wrong — thin on the sides, heavy top and bottom. Hover does not draw a line: it wipes the corrected stroke across the button from the leading edge, so what you watch is a border becoming even rather than becoming present. Corners are square on purpose, which isolates the stroke error from the geometry error.
Nucleo: 44,000+ SVG icons in five families · All Icons Bundle $149
Nucleo · Affiliate link
The code
<section class="svgb-10">
<input class="svgb-10__ti" type="checkbox" id="svgb-10-theme">
<label class="svgb-10__toggle" for="svgb-10-theme">
<svg 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="svgb-10__sr">Switch colour scheme</span>
</label>
<div class="svgb-10__stage">
<div class="svgb-10__sheet">
<p class="svgb-10__kicker">One component · six locales · zero fixed widths</p>
<ul class="svgb-10__rows">
<li class="svgb-10__row">
<span class="svgb-10__loc">en</span>
<button class="svgb-10__btn" type="button">
<svg class="svgb-10__frame" viewBox="0 0 200 56" preserveAspectRatio="none" aria-hidden="true"><rect class="svgb-10__raw" x="1" y="1" width="198" height="54"/></svg>
<svg class="svgb-10__frame svgb-10__frame--true" viewBox="0 0 200 56" preserveAspectRatio="none" aria-hidden="true"><rect class="svgb-10__true" x="1" y="1" width="198" height="54" vector-effect="non-scaling-stroke"/></svg>
<span class="svgb-10__seam" aria-hidden="true"></span>
<span class="svgb-10__label">Start free trial</span>
</button>
</li>
<li class="svgb-10__row">
<span class="svgb-10__loc">de</span>
<button class="svgb-10__btn" type="button">
<svg class="svgb-10__frame" viewBox="0 0 200 56" preserveAspectRatio="none" aria-hidden="true"><rect class="svgb-10__raw" x="1" y="1" width="198" height="54"/></svg>
<svg class="svgb-10__frame svgb-10__frame--true" viewBox="0 0 200 56" preserveAspectRatio="none" aria-hidden="true"><rect class="svgb-10__true" x="1" y="1" width="198" height="54" vector-effect="non-scaling-stroke"/></svg>
<span class="svgb-10__seam" aria-hidden="true"></span>
<span class="svgb-10__label" lang="de">Kostenlos testen starten</span>
</button>
</li>
<li class="svgb-10__row">
<span class="svgb-10__loc">fi</span>
<button class="svgb-10__btn" type="button">
<svg class="svgb-10__frame" viewBox="0 0 200 56" preserveAspectRatio="none" aria-hidden="true"><rect class="svgb-10__raw" x="1" y="1" width="198" height="54"/></svg>
<svg class="svgb-10__frame svgb-10__frame--true" viewBox="0 0 200 56" preserveAspectRatio="none" aria-hidden="true"><rect class="svgb-10__true" x="1" y="1" width="198" height="54" vector-effect="non-scaling-stroke"/></svg>
<span class="svgb-10__seam" aria-hidden="true"></span>
<span class="svgb-10__label" lang="fi">Aloita maksuton kokeilujakso</span>
</button>
</li>
<li class="svgb-10__row">
<span class="svgb-10__loc">ja</span>
<button class="svgb-10__btn" type="button">
<svg class="svgb-10__frame" viewBox="0 0 200 56" preserveAspectRatio="none" aria-hidden="true"><rect class="svgb-10__raw" x="1" y="1" width="198" height="54"/></svg>
<svg class="svgb-10__frame svgb-10__frame--true" viewBox="0 0 200 56" preserveAspectRatio="none" aria-hidden="true"><rect class="svgb-10__true" x="1" y="1" width="198" height="54" vector-effect="non-scaling-stroke"/></svg>
<span class="svgb-10__seam" aria-hidden="true"></span>
<span class="svgb-10__label" lang="ja">無料デモ</span>
</button>
</li>
<li class="svgb-10__row" dir="rtl">
<span class="svgb-10__loc">ar</span>
<button class="svgb-10__btn" type="button">
<svg class="svgb-10__frame" viewBox="0 0 200 56" preserveAspectRatio="none" aria-hidden="true"><rect class="svgb-10__raw" x="1" y="1" width="198" height="54"/></svg>
<svg class="svgb-10__frame svgb-10__frame--true" viewBox="0 0 200 56" preserveAspectRatio="none" aria-hidden="true"><rect class="svgb-10__true" x="1" y="1" width="198" height="54" vector-effect="non-scaling-stroke"/></svg>
<span class="svgb-10__seam" aria-hidden="true"></span>
<span class="svgb-10__label" lang="ar">ابدأ النسخة المجانية</span>
</button>
</li>
<li class="svgb-10__row">
<span class="svgb-10__loc">live</span>
<button class="svgb-10__btn svgb-10__btn--edit" type="button">
<svg class="svgb-10__frame" viewBox="0 0 200 56" preserveAspectRatio="none" aria-hidden="true"><rect class="svgb-10__raw" x="1" y="1" width="198" height="54"/></svg>
<svg class="svgb-10__frame svgb-10__frame--true" viewBox="0 0 200 56" preserveAspectRatio="none" aria-hidden="true"><rect class="svgb-10__true" x="1" y="1" width="198" height="54" vector-effect="non-scaling-stroke"/></svg>
<span class="svgb-10__seam" aria-hidden="true"></span>
<span class="svgb-10__label" contenteditable="true" spellcheck="false">Type to resize the frame</span>
</button>
</li>
</ul>
<div class="svgb-10__ab">
<div class="svgb-10__abcell">
<div class="svgb-10__wide">
<svg class="svgb-10__vb" viewBox="0 0 200 56" preserveAspectRatio="none" aria-hidden="true">
<rect class="svgb-10__vbline" x="2" y="2" width="196" height="52" rx="14" vector-effect="non-scaling-stroke"/>
</svg>
<span class="svgb-10__wl">non-scaling-stroke</span>
</div>
<code class="svgb-10__cap">even 2px on all four edges</code>
</div>
<div class="svgb-10__abcell">
<div class="svgb-10__wide">
<svg class="svgb-10__vb" viewBox="0 0 200 56" preserveAspectRatio="none" aria-hidden="true">
<rect class="svgb-10__vbline svgb-10__vbline--raw" x="2" y="2" width="196" height="52" rx="14"/>
</svg>
<span class="svgb-10__wl">unprotected</span>
</div>
<code class="svgb-10__cap">uneven · the more-scaled axis paints thicker</code>
</div>
</div>
</div>
</div>
</section><section class="svgb-10">
<input class="svgb-10__ti" type="checkbox" id="svgb-10-theme">
<label class="svgb-10__toggle" for="svgb-10-theme">
<svg 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="svgb-10__sr">Switch colour scheme</span>
</label>
<div class="svgb-10__stage">
<div class="svgb-10__sheet">
<p class="svgb-10__kicker">One component · six locales · zero fixed widths</p>
<ul class="svgb-10__rows">
<li class="svgb-10__row">
<span class="svgb-10__loc">en</span>
<button class="svgb-10__btn" type="button">
<svg class="svgb-10__frame" viewBox="0 0 200 56" preserveAspectRatio="none" aria-hidden="true"><rect class="svgb-10__raw" x="1" y="1" width="198" height="54"/></svg>
<svg class="svgb-10__frame svgb-10__frame--true" viewBox="0 0 200 56" preserveAspectRatio="none" aria-hidden="true"><rect class="svgb-10__true" x="1" y="1" width="198" height="54" vector-effect="non-scaling-stroke"/></svg>
<span class="svgb-10__seam" aria-hidden="true"></span>
<span class="svgb-10__label">Start free trial</span>
</button>
</li>
<li class="svgb-10__row">
<span class="svgb-10__loc">de</span>
<button class="svgb-10__btn" type="button">
<svg class="svgb-10__frame" viewBox="0 0 200 56" preserveAspectRatio="none" aria-hidden="true"><rect class="svgb-10__raw" x="1" y="1" width="198" height="54"/></svg>
<svg class="svgb-10__frame svgb-10__frame--true" viewBox="0 0 200 56" preserveAspectRatio="none" aria-hidden="true"><rect class="svgb-10__true" x="1" y="1" width="198" height="54" vector-effect="non-scaling-stroke"/></svg>
<span class="svgb-10__seam" aria-hidden="true"></span>
<span class="svgb-10__label" lang="de">Kostenlos testen starten</span>
</button>
</li>
<li class="svgb-10__row">
<span class="svgb-10__loc">fi</span>
<button class="svgb-10__btn" type="button">
<svg class="svgb-10__frame" viewBox="0 0 200 56" preserveAspectRatio="none" aria-hidden="true"><rect class="svgb-10__raw" x="1" y="1" width="198" height="54"/></svg>
<svg class="svgb-10__frame svgb-10__frame--true" viewBox="0 0 200 56" preserveAspectRatio="none" aria-hidden="true"><rect class="svgb-10__true" x="1" y="1" width="198" height="54" vector-effect="non-scaling-stroke"/></svg>
<span class="svgb-10__seam" aria-hidden="true"></span>
<span class="svgb-10__label" lang="fi">Aloita maksuton kokeilujakso</span>
</button>
</li>
<li class="svgb-10__row">
<span class="svgb-10__loc">ja</span>
<button class="svgb-10__btn" type="button">
<svg class="svgb-10__frame" viewBox="0 0 200 56" preserveAspectRatio="none" aria-hidden="true"><rect class="svgb-10__raw" x="1" y="1" width="198" height="54"/></svg>
<svg class="svgb-10__frame svgb-10__frame--true" viewBox="0 0 200 56" preserveAspectRatio="none" aria-hidden="true"><rect class="svgb-10__true" x="1" y="1" width="198" height="54" vector-effect="non-scaling-stroke"/></svg>
<span class="svgb-10__seam" aria-hidden="true"></span>
<span class="svgb-10__label" lang="ja">無料デモ</span>
</button>
</li>
<li class="svgb-10__row" dir="rtl">
<span class="svgb-10__loc">ar</span>
<button class="svgb-10__btn" type="button">
<svg class="svgb-10__frame" viewBox="0 0 200 56" preserveAspectRatio="none" aria-hidden="true"><rect class="svgb-10__raw" x="1" y="1" width="198" height="54"/></svg>
<svg class="svgb-10__frame svgb-10__frame--true" viewBox="0 0 200 56" preserveAspectRatio="none" aria-hidden="true"><rect class="svgb-10__true" x="1" y="1" width="198" height="54" vector-effect="non-scaling-stroke"/></svg>
<span class="svgb-10__seam" aria-hidden="true"></span>
<span class="svgb-10__label" lang="ar">ابدأ النسخة المجانية</span>
</button>
</li>
<li class="svgb-10__row">
<span class="svgb-10__loc">live</span>
<button class="svgb-10__btn svgb-10__btn--edit" type="button">
<svg class="svgb-10__frame" viewBox="0 0 200 56" preserveAspectRatio="none" aria-hidden="true"><rect class="svgb-10__raw" x="1" y="1" width="198" height="54"/></svg>
<svg class="svgb-10__frame svgb-10__frame--true" viewBox="0 0 200 56" preserveAspectRatio="none" aria-hidden="true"><rect class="svgb-10__true" x="1" y="1" width="198" height="54" vector-effect="non-scaling-stroke"/></svg>
<span class="svgb-10__seam" aria-hidden="true"></span>
<span class="svgb-10__label" contenteditable="true" spellcheck="false">Type to resize the frame</span>
</button>
</li>
</ul>
<div class="svgb-10__ab">
<div class="svgb-10__abcell">
<div class="svgb-10__wide">
<svg class="svgb-10__vb" viewBox="0 0 200 56" preserveAspectRatio="none" aria-hidden="true">
<rect class="svgb-10__vbline" x="2" y="2" width="196" height="52" rx="14" vector-effect="non-scaling-stroke"/>
</svg>
<span class="svgb-10__wl">non-scaling-stroke</span>
</div>
<code class="svgb-10__cap">even 2px on all four edges</code>
</div>
<div class="svgb-10__abcell">
<div class="svgb-10__wide">
<svg class="svgb-10__vb" viewBox="0 0 200 56" preserveAspectRatio="none" aria-hidden="true">
<rect class="svgb-10__vbline svgb-10__vbline--raw" x="2" y="2" width="196" height="52" rx="14"/>
</svg>
<span class="svgb-10__wl">unprotected</span>
</div>
<code class="svgb-10__cap">uneven · the more-scaled axis paints thicker</code>
</div>
</div>
</div>
</div>
</section>@import url('https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Inter+Tight:wght@400;500;600&family=JetBrains+Mono:wght@400&display=swap');
.svgb-10 {
--svgb-10-bg: #f7f5f0;
--svgb-10-bg: oklch(0.972 0.008 90);
--svgb-10-fg: #17161a;
--svgb-10-fg: oklch(0.20 0.006 290);
--svgb-10-muted: #6c6a75;
--svgb-10-muted: oklch(0.555 0.012 290);
--svgb-10-rest: rgba(23, 22, 26, 0.16);
--svgb-10-rest: oklch(0.20 0.006 290 / 0.16);
--svgb-10-stroke: #5b21f0;
--svgb-10-stroke: oklch(0.46 0.26 295);
--svgb-10-warn: #c4341f;
--svgb-10-warn: oklch(0.53 0.19 32);
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
background: var(--svgb-10-bg);
position: relative;
color: var(--svgb-10-fg);
font-family: 'Inter Tight', system-ui, sans-serif;
}
.svgb-10 *,
.svgb-10 *::before,
.svgb-10 *::after {
box-sizing: border-box;
}
.svgb-10__stage {
min-height: 100svh;
display: grid;
place-items: center;
padding: clamp(22px, 5vw, 68px);
}
.svgb-10__sheet {
width: 100%;
max-width: 880px;
display: flex;
flex-direction: column;
gap: clamp(20px, 3vw, 32px);
}
.svgb-10__kicker {
margin: 0;
font-family: 'Instrument Serif', ui-serif, Georgia, serif;
font-size: clamp(22px, 3.6vw, 38px);
font-style: italic;
letter-spacing: -0.02em;
line-height: 1.15;
}
.svgb-10__rows {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 14px;
}
.svgb-10__row {
display: flex;
align-items: center;
gap: 16px;
padding-block: 4px;
border-block-start: 1px solid var(--svgb-10-rest);
padding-block-start: 14px;
}
.svgb-10__loc {
flex: none;
inline-size: 42px;
font-family: 'JetBrains Mono', ui-monospace, monospace;
font-size: 10.5px;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--svgb-10-muted);
}
.svgb-10__btn {
position: relative;
display: inline-grid;
place-items: center;
min-inline-size: 92px;
min-block-size: 56px;
padding: 0 26px;
border: 0;
border-radius: 0;
background: transparent;
color: inherit;
font: inherit;
font-size: 15px;
font-weight: 500;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
}
.svgb-10__btn:focus-visible {
outline: 2px solid var(--svgb-10-stroke);
outline-offset: 3px;
}
.svgb-10__label {
position: relative;
z-index: 1;
outline: none;
}
.svgb-10__btn--edit .svgb-10__label {
min-inline-size: 4ch;
caret-color: var(--svgb-10-stroke);
}
.svgb-10__frame {
position: absolute;
inset: 0;
inline-size: 100%;
block-size: 100%;
overflow: visible;
}
.svgb-10__frame rect {
fill: none;
}
/* the resting edge is the UNPROTECTED stroke: one fixed viewBox stretched to the
button's width, so the sides thin out as the label grows */
.svgb-10__raw {
stroke: var(--svgb-10-rest);
stroke-width: 2px;
transition: stroke 240ms ease;
}
.svgb-10__btn:hover .svgb-10__raw,
.svgb-10__btn:focus-visible .svgb-10__raw {
stroke: color-mix(in oklab, var(--svgb-10-warn) 45%, transparent);
}
/* hover wipes the corrected stroke across it — a clip-path reveal, not a dash draw,
so what you watch is the line becoming even rather than becoming present */
.svgb-10__frame--true {
clip-path: inset(0 100% 0 0);
transition: clip-path 760ms cubic-bezier(0.22, 1, 0.36, 1);
}
.svgb-10__true {
stroke: var(--svgb-10-stroke);
stroke-width: 2px;
vector-effect: non-scaling-stroke;
}
/* inset() is physical, so RTL needs its own resting edge or the reveal fights the seam */
[dir="rtl"] .svgb-10__frame--true {
clip-path: inset(0 0 0 100%);
}
.svgb-10__btn:hover .svgb-10__frame--true,
.svgb-10__btn:focus-visible .svgb-10__frame--true {
clip-path: inset(0 0 0 0);
}
.svgb-10__seam {
position: absolute;
inset-block: -4px;
inline-size: 1.5px;
inset-inline-start: 0;
background: var(--svgb-10-stroke);
opacity: 0;
pointer-events: none;
}
.svgb-10__btn:hover .svgb-10__seam,
.svgb-10__btn:focus-visible .svgb-10__seam {
animation: svgb-10-seam 760ms cubic-bezier(0.22, 1, 0.36, 1) forwards;
}
@keyframes svgb-10-seam {
0% {
inset-inline-start: 0;
opacity: 0;
}
10% {
opacity: 1;
}
86% {
opacity: 1;
}
100% {
inset-inline-start: 100%;
opacity: 0;
}
}
.svgb-10__ab {
display: grid;
gap: clamp(16px, 3vw, 28px);
grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr));
padding-block-start: 6px;
border-block-start: 1px solid var(--svgb-10-rest);
}
.svgb-10__abcell {
display: flex;
flex-direction: column;
gap: 8px;
}
.svgb-10__wide {
position: relative;
display: grid;
place-items: center;
block-size: 88px;
}
.svgb-10__vb {
position: absolute;
inset: 0;
inline-size: 100%;
block-size: 100%;
overflow: visible;
}
.svgb-10__vbline {
fill: none;
stroke: var(--svgb-10-stroke);
stroke-width: 2px;
}
.svgb-10__vbline--raw {
stroke: #c4341f;
stroke: oklch(0.53 0.19 32);
}
.svgb-10__wl {
position: relative;
font-family: 'JetBrains Mono', ui-monospace, monospace;
font-size: 12px;
}
.svgb-10__cap {
font-family: 'JetBrains Mono', ui-monospace, monospace;
font-size: 11px;
color: var(--svgb-10-muted);
}
.svgb-10__ti {
position: absolute;
opacity: 0;
pointer-events: none;
}
.svgb-10__toggle {
position: absolute;
z-index: 5;
inset-block-start: 16px;
inset-inline-end: 16px;
display: grid;
place-items: center;
inline-size: 44px;
block-size: 44px;
border-radius: 999px;
border: 1px solid color-mix(in oklab, currentColor 20%, transparent);
background: color-mix(in oklab, currentColor 7%, transparent);
color: inherit;
cursor: pointer;
transition: background 160ms ease, transform 160ms ease;
}
.svgb-10__toggle:hover {
background: color-mix(in oklab, currentColor 14%, transparent);
}
.svgb-10__toggle:active {
transform: scale(0.94);
}
.svgb-10__toggle svg {
inline-size: 18px;
block-size: 18px;
transition: transform 400ms cubic-bezier(0.2, 0.7, 0.3, 1);
}
.svgb-10__ti:checked + .svgb-10__toggle svg {
transform: rotate(180deg);
}
.svgb-10__ti:focus-visible + .svgb-10__toggle {
outline: 2px solid currentColor;
outline-offset: 3px;
}
.svgb-10__sr {
position: absolute;
inline-size: 1px;
block-size: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
.svgb-10:has(#svgb-10-theme:checked) {
--svgb-10-bg: #0f0e12;
--svgb-10-bg: oklch(0.18 0.006 290);
--svgb-10-fg: #f0eff4;
--svgb-10-fg: oklch(0.96 0.004 290);
--svgb-10-muted: #918f9c;
--svgb-10-muted: oklch(0.68 0.012 290);
--svgb-10-rest: rgba(240, 239, 244, 0.18);
--svgb-10-rest: oklch(0.96 0.004 290 / 0.18);
--svgb-10-stroke: #c4a8ff;
--svgb-10-stroke: oklch(0.82 0.15 295);
}
.svgb-10:has(#svgb-10-theme:checked) .svgb-10__vbline--raw {
stroke: oklch(0.75 0.16 32);
}
@media (prefers-reduced-motion: reduce) {
.svgb-10__frame--true {
transition: none;
}
.svgb-10__seam {
display: none;
}
.svgb-10__toggle,
.svgb-10__toggle svg {
transition: none;
}
}@import url('https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Inter+Tight:wght@400;500;600&family=JetBrains+Mono:wght@400&display=swap');
.svgb-10 {
--svgb-10-bg: #f7f5f0;
--svgb-10-bg: oklch(0.972 0.008 90);
--svgb-10-fg: #17161a;
--svgb-10-fg: oklch(0.20 0.006 290);
--svgb-10-muted: #6c6a75;
--svgb-10-muted: oklch(0.555 0.012 290);
--svgb-10-rest: rgba(23, 22, 26, 0.16);
--svgb-10-rest: oklch(0.20 0.006 290 / 0.16);
--svgb-10-stroke: #5b21f0;
--svgb-10-stroke: oklch(0.46 0.26 295);
--svgb-10-warn: #c4341f;
--svgb-10-warn: oklch(0.53 0.19 32);
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
background: var(--svgb-10-bg);
position: relative;
color: var(--svgb-10-fg);
font-family: 'Inter Tight', system-ui, sans-serif;
}
.svgb-10 *,
.svgb-10 *::before,
.svgb-10 *::after {
box-sizing: border-box;
}
.svgb-10__stage {
min-height: 100svh;
display: grid;
place-items: center;
padding: clamp(22px, 5vw, 68px);
}
.svgb-10__sheet {
width: 100%;
max-width: 880px;
display: flex;
flex-direction: column;
gap: clamp(20px, 3vw, 32px);
}
.svgb-10__kicker {
margin: 0;
font-family: 'Instrument Serif', ui-serif, Georgia, serif;
font-size: clamp(22px, 3.6vw, 38px);
font-style: italic;
letter-spacing: -0.02em;
line-height: 1.15;
}
.svgb-10__rows {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 14px;
}
.svgb-10__row {
display: flex;
align-items: center;
gap: 16px;
padding-block: 4px;
border-block-start: 1px solid var(--svgb-10-rest);
padding-block-start: 14px;
}
.svgb-10__loc {
flex: none;
inline-size: 42px;
font-family: 'JetBrains Mono', ui-monospace, monospace;
font-size: 10.5px;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--svgb-10-muted);
}
.svgb-10__btn {
position: relative;
display: inline-grid;
place-items: center;
min-inline-size: 92px;
min-block-size: 56px;
padding: 0 26px;
border: 0;
border-radius: 0;
background: transparent;
color: inherit;
font: inherit;
font-size: 15px;
font-weight: 500;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
}
.svgb-10__btn:focus-visible {
outline: 2px solid var(--svgb-10-stroke);
outline-offset: 3px;
}
.svgb-10__label {
position: relative;
z-index: 1;
outline: none;
}
.svgb-10__btn--edit .svgb-10__label {
min-inline-size: 4ch;
caret-color: var(--svgb-10-stroke);
}
.svgb-10__frame {
position: absolute;
inset: 0;
inline-size: 100%;
block-size: 100%;
overflow: visible;
}
.svgb-10__frame rect {
fill: none;
}
/* the resting edge is the UNPROTECTED stroke: one fixed viewBox stretched to the
button's width, so the sides thin out as the label grows */
.svgb-10__raw {
stroke: var(--svgb-10-rest);
stroke-width: 2px;
transition: stroke 240ms ease;
}
.svgb-10__btn:hover .svgb-10__raw,
.svgb-10__btn:focus-visible .svgb-10__raw {
stroke: color-mix(in oklab, var(--svgb-10-warn) 45%, transparent);
}
/* hover wipes the corrected stroke across it — a clip-path reveal, not a dash draw,
so what you watch is the line becoming even rather than becoming present */
.svgb-10__frame--true {
clip-path: inset(0 100% 0 0);
transition: clip-path 760ms cubic-bezier(0.22, 1, 0.36, 1);
}
.svgb-10__true {
stroke: var(--svgb-10-stroke);
stroke-width: 2px;
vector-effect: non-scaling-stroke;
}
/* inset() is physical, so RTL needs its own resting edge or the reveal fights the seam */
[dir="rtl"] .svgb-10__frame--true {
clip-path: inset(0 0 0 100%);
}
.svgb-10__btn:hover .svgb-10__frame--true,
.svgb-10__btn:focus-visible .svgb-10__frame--true {
clip-path: inset(0 0 0 0);
}
.svgb-10__seam {
position: absolute;
inset-block: -4px;
inline-size: 1.5px;
inset-inline-start: 0;
background: var(--svgb-10-stroke);
opacity: 0;
pointer-events: none;
}
.svgb-10__btn:hover .svgb-10__seam,
.svgb-10__btn:focus-visible .svgb-10__seam {
animation: svgb-10-seam 760ms cubic-bezier(0.22, 1, 0.36, 1) forwards;
}
@keyframes svgb-10-seam {
0% {
inset-inline-start: 0;
opacity: 0;
}
10% {
opacity: 1;
}
86% {
opacity: 1;
}
100% {
inset-inline-start: 100%;
opacity: 0;
}
}
.svgb-10__ab {
display: grid;
gap: clamp(16px, 3vw, 28px);
grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr));
padding-block-start: 6px;
border-block-start: 1px solid var(--svgb-10-rest);
}
.svgb-10__abcell {
display: flex;
flex-direction: column;
gap: 8px;
}
.svgb-10__wide {
position: relative;
display: grid;
place-items: center;
block-size: 88px;
}
.svgb-10__vb {
position: absolute;
inset: 0;
inline-size: 100%;
block-size: 100%;
overflow: visible;
}
.svgb-10__vbline {
fill: none;
stroke: var(--svgb-10-stroke);
stroke-width: 2px;
}
.svgb-10__vbline--raw {
stroke: #c4341f;
stroke: oklch(0.53 0.19 32);
}
.svgb-10__wl {
position: relative;
font-family: 'JetBrains Mono', ui-monospace, monospace;
font-size: 12px;
}
.svgb-10__cap {
font-family: 'JetBrains Mono', ui-monospace, monospace;
font-size: 11px;
color: var(--svgb-10-muted);
}
.svgb-10__ti {
position: absolute;
opacity: 0;
pointer-events: none;
}
.svgb-10__toggle {
position: absolute;
z-index: 5;
inset-block-start: 16px;
inset-inline-end: 16px;
display: grid;
place-items: center;
inline-size: 44px;
block-size: 44px;
border-radius: 999px;
border: 1px solid color-mix(in oklab, currentColor 20%, transparent);
background: color-mix(in oklab, currentColor 7%, transparent);
color: inherit;
cursor: pointer;
transition: background 160ms ease, transform 160ms ease;
}
.svgb-10__toggle:hover {
background: color-mix(in oklab, currentColor 14%, transparent);
}
.svgb-10__toggle:active {
transform: scale(0.94);
}
.svgb-10__toggle svg {
inline-size: 18px;
block-size: 18px;
transition: transform 400ms cubic-bezier(0.2, 0.7, 0.3, 1);
}
.svgb-10__ti:checked + .svgb-10__toggle svg {
transform: rotate(180deg);
}
.svgb-10__ti:focus-visible + .svgb-10__toggle {
outline: 2px solid currentColor;
outline-offset: 3px;
}
.svgb-10__sr {
position: absolute;
inline-size: 1px;
block-size: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
.svgb-10:has(#svgb-10-theme:checked) {
--svgb-10-bg: #0f0e12;
--svgb-10-bg: oklch(0.18 0.006 290);
--svgb-10-fg: #f0eff4;
--svgb-10-fg: oklch(0.96 0.004 290);
--svgb-10-muted: #918f9c;
--svgb-10-muted: oklch(0.68 0.012 290);
--svgb-10-rest: rgba(240, 239, 244, 0.18);
--svgb-10-rest: oklch(0.96 0.004 290 / 0.18);
--svgb-10-stroke: #c4a8ff;
--svgb-10-stroke: oklch(0.82 0.15 295);
}
.svgb-10:has(#svgb-10-theme:checked) .svgb-10__vbline--raw {
stroke: oklch(0.75 0.16 32);
}
@media (prefers-reduced-motion: reduce) {
.svgb-10__frame--true {
transition: none;
}
.svgb-10__seam {
display: none;
}
.svgb-10__toggle,
.svgb-10__toggle svg {
transition: none;
}
}Here's a working CSS SVG Stroke Button 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: Non Scaling Stroke Button That Resizes with Text
Source: https://codefronts.com/components/css-svg-stroke-buttons/non-scaling-stroke-button-that-resizes-with-text/
The same call to action in six languages, right-to-left included, each button sized by its own text. Every frame is one fixed 200×56 viewBox stretched to whatever width the label needs, so the resting border is genuinely wrong — thin on the sides, heavy top and bottom. Hover does not draw a line: it wipes the corrected stroke across the button from the leading edge, so what you watch is a border becoming even rather than becoming present. Corners are square on purpose, which isolates the stroke error from the geometry error.
## HTML
```html
<section class="svgb-10">
<input class="svgb-10__ti" type="checkbox" id="svgb-10-theme">
<label class="svgb-10__toggle" for="svgb-10-theme">
<svg 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="svgb-10__sr">Switch colour scheme</span>
</label>
<div class="svgb-10__stage">
<div class="svgb-10__sheet">
<p class="svgb-10__kicker">One component · six locales · zero fixed widths</p>
<ul class="svgb-10__rows">
<li class="svgb-10__row">
<span class="svgb-10__loc">en</span>
<button class="svgb-10__btn" type="button">
<svg class="svgb-10__frame" viewBox="0 0 200 56" preserveAspectRatio="none" aria-hidden="true"><rect class="svgb-10__raw" x="1" y="1" width="198" height="54"/></svg>
<svg class="svgb-10__frame svgb-10__frame--true" viewBox="0 0 200 56" preserveAspectRatio="none" aria-hidden="true"><rect class="svgb-10__true" x="1" y="1" width="198" height="54" vector-effect="non-scaling-stroke"/></svg>
<span class="svgb-10__seam" aria-hidden="true"></span>
<span class="svgb-10__label">Start free trial</span>
</button>
</li>
<li class="svgb-10__row">
<span class="svgb-10__loc">de</span>
<button class="svgb-10__btn" type="button">
<svg class="svgb-10__frame" viewBox="0 0 200 56" preserveAspectRatio="none" aria-hidden="true"><rect class="svgb-10__raw" x="1" y="1" width="198" height="54"/></svg>
<svg class="svgb-10__frame svgb-10__frame--true" viewBox="0 0 200 56" preserveAspectRatio="none" aria-hidden="true"><rect class="svgb-10__true" x="1" y="1" width="198" height="54" vector-effect="non-scaling-stroke"/></svg>
<span class="svgb-10__seam" aria-hidden="true"></span>
<span class="svgb-10__label" lang="de">Kostenlos testen starten</span>
</button>
</li>
<li class="svgb-10__row">
<span class="svgb-10__loc">fi</span>
<button class="svgb-10__btn" type="button">
<svg class="svgb-10__frame" viewBox="0 0 200 56" preserveAspectRatio="none" aria-hidden="true"><rect class="svgb-10__raw" x="1" y="1" width="198" height="54"/></svg>
<svg class="svgb-10__frame svgb-10__frame--true" viewBox="0 0 200 56" preserveAspectRatio="none" aria-hidden="true"><rect class="svgb-10__true" x="1" y="1" width="198" height="54" vector-effect="non-scaling-stroke"/></svg>
<span class="svgb-10__seam" aria-hidden="true"></span>
<span class="svgb-10__label" lang="fi">Aloita maksuton kokeilujakso</span>
</button>
</li>
<li class="svgb-10__row">
<span class="svgb-10__loc">ja</span>
<button class="svgb-10__btn" type="button">
<svg class="svgb-10__frame" viewBox="0 0 200 56" preserveAspectRatio="none" aria-hidden="true"><rect class="svgb-10__raw" x="1" y="1" width="198" height="54"/></svg>
<svg class="svgb-10__frame svgb-10__frame--true" viewBox="0 0 200 56" preserveAspectRatio="none" aria-hidden="true"><rect class="svgb-10__true" x="1" y="1" width="198" height="54" vector-effect="non-scaling-stroke"/></svg>
<span class="svgb-10__seam" aria-hidden="true"></span>
<span class="svgb-10__label" lang="ja">無料デモ</span>
</button>
</li>
<li class="svgb-10__row" dir="rtl">
<span class="svgb-10__loc">ar</span>
<button class="svgb-10__btn" type="button">
<svg class="svgb-10__frame" viewBox="0 0 200 56" preserveAspectRatio="none" aria-hidden="true"><rect class="svgb-10__raw" x="1" y="1" width="198" height="54"/></svg>
<svg class="svgb-10__frame svgb-10__frame--true" viewBox="0 0 200 56" preserveAspectRatio="none" aria-hidden="true"><rect class="svgb-10__true" x="1" y="1" width="198" height="54" vector-effect="non-scaling-stroke"/></svg>
<span class="svgb-10__seam" aria-hidden="true"></span>
<span class="svgb-10__label" lang="ar">ابدأ النسخة المجانية</span>
</button>
</li>
<li class="svgb-10__row">
<span class="svgb-10__loc">live</span>
<button class="svgb-10__btn svgb-10__btn--edit" type="button">
<svg class="svgb-10__frame" viewBox="0 0 200 56" preserveAspectRatio="none" aria-hidden="true"><rect class="svgb-10__raw" x="1" y="1" width="198" height="54"/></svg>
<svg class="svgb-10__frame svgb-10__frame--true" viewBox="0 0 200 56" preserveAspectRatio="none" aria-hidden="true"><rect class="svgb-10__true" x="1" y="1" width="198" height="54" vector-effect="non-scaling-stroke"/></svg>
<span class="svgb-10__seam" aria-hidden="true"></span>
<span class="svgb-10__label" contenteditable="true" spellcheck="false">Type to resize the frame</span>
</button>
</li>
</ul>
<div class="svgb-10__ab">
<div class="svgb-10__abcell">
<div class="svgb-10__wide">
<svg class="svgb-10__vb" viewBox="0 0 200 56" preserveAspectRatio="none" aria-hidden="true">
<rect class="svgb-10__vbline" x="2" y="2" width="196" height="52" rx="14" vector-effect="non-scaling-stroke"/>
</svg>
<span class="svgb-10__wl">non-scaling-stroke</span>
</div>
<code class="svgb-10__cap">even 2px on all four edges</code>
</div>
<div class="svgb-10__abcell">
<div class="svgb-10__wide">
<svg class="svgb-10__vb" viewBox="0 0 200 56" preserveAspectRatio="none" aria-hidden="true">
<rect class="svgb-10__vbline svgb-10__vbline--raw" x="2" y="2" width="196" height="52" rx="14"/>
</svg>
<span class="svgb-10__wl">unprotected</span>
</div>
<code class="svgb-10__cap">uneven · the more-scaled axis paints thicker</code>
</div>
</div>
</div>
</div>
</section>
```
## CSS
```css
@import url('https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Inter+Tight:wght@400;500;600&family=JetBrains+Mono:wght@400&display=swap');
.svgb-10 {
--svgb-10-bg: #f7f5f0;
--svgb-10-bg: oklch(0.972 0.008 90);
--svgb-10-fg: #17161a;
--svgb-10-fg: oklch(0.20 0.006 290);
--svgb-10-muted: #6c6a75;
--svgb-10-muted: oklch(0.555 0.012 290);
--svgb-10-rest: rgba(23, 22, 26, 0.16);
--svgb-10-rest: oklch(0.20 0.006 290 / 0.16);
--svgb-10-stroke: #5b21f0;
--svgb-10-stroke: oklch(0.46 0.26 295);
--svgb-10-warn: #c4341f;
--svgb-10-warn: oklch(0.53 0.19 32);
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
background: var(--svgb-10-bg);
position: relative;
color: var(--svgb-10-fg);
font-family: 'Inter Tight', system-ui, sans-serif;
}
.svgb-10 *,
.svgb-10 *::before,
.svgb-10 *::after {
box-sizing: border-box;
}
.svgb-10__stage {
min-height: 100svh;
display: grid;
place-items: center;
padding: clamp(22px, 5vw, 68px);
}
.svgb-10__sheet {
width: 100%;
max-width: 880px;
display: flex;
flex-direction: column;
gap: clamp(20px, 3vw, 32px);
}
.svgb-10__kicker {
margin: 0;
font-family: 'Instrument Serif', ui-serif, Georgia, serif;
font-size: clamp(22px, 3.6vw, 38px);
font-style: italic;
letter-spacing: -0.02em;
line-height: 1.15;
}
.svgb-10__rows {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 14px;
}
.svgb-10__row {
display: flex;
align-items: center;
gap: 16px;
padding-block: 4px;
border-block-start: 1px solid var(--svgb-10-rest);
padding-block-start: 14px;
}
.svgb-10__loc {
flex: none;
inline-size: 42px;
font-family: 'JetBrains Mono', ui-monospace, monospace;
font-size: 10.5px;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--svgb-10-muted);
}
.svgb-10__btn {
position: relative;
display: inline-grid;
place-items: center;
min-inline-size: 92px;
min-block-size: 56px;
padding: 0 26px;
border: 0;
border-radius: 0;
background: transparent;
color: inherit;
font: inherit;
font-size: 15px;
font-weight: 500;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
}
.svgb-10__btn:focus-visible {
outline: 2px solid var(--svgb-10-stroke);
outline-offset: 3px;
}
.svgb-10__label {
position: relative;
z-index: 1;
outline: none;
}
.svgb-10__btn--edit .svgb-10__label {
min-inline-size: 4ch;
caret-color: var(--svgb-10-stroke);
}
.svgb-10__frame {
position: absolute;
inset: 0;
inline-size: 100%;
block-size: 100%;
overflow: visible;
}
.svgb-10__frame rect {
fill: none;
}
/* the resting edge is the UNPROTECTED stroke: one fixed viewBox stretched to the
button's width, so the sides thin out as the label grows */
.svgb-10__raw {
stroke: var(--svgb-10-rest);
stroke-width: 2px;
transition: stroke 240ms ease;
}
.svgb-10__btn:hover .svgb-10__raw,
.svgb-10__btn:focus-visible .svgb-10__raw {
stroke: color-mix(in oklab, var(--svgb-10-warn) 45%, transparent);
}
/* hover wipes the corrected stroke across it — a clip-path reveal, not a dash draw,
so what you watch is the line becoming even rather than becoming present */
.svgb-10__frame--true {
clip-path: inset(0 100% 0 0);
transition: clip-path 760ms cubic-bezier(0.22, 1, 0.36, 1);
}
.svgb-10__true {
stroke: var(--svgb-10-stroke);
stroke-width: 2px;
vector-effect: non-scaling-stroke;
}
/* inset() is physical, so RTL needs its own resting edge or the reveal fights the seam */
[dir="rtl"] .svgb-10__frame--true {
clip-path: inset(0 0 0 100%);
}
.svgb-10__btn:hover .svgb-10__frame--true,
.svgb-10__btn:focus-visible .svgb-10__frame--true {
clip-path: inset(0 0 0 0);
}
.svgb-10__seam {
position: absolute;
inset-block: -4px;
inline-size: 1.5px;
inset-inline-start: 0;
background: var(--svgb-10-stroke);
opacity: 0;
pointer-events: none;
}
.svgb-10__btn:hover .svgb-10__seam,
.svgb-10__btn:focus-visible .svgb-10__seam {
animation: svgb-10-seam 760ms cubic-bezier(0.22, 1, 0.36, 1) forwards;
}
@keyframes svgb-10-seam {
0% {
inset-inline-start: 0;
opacity: 0;
}
10% {
opacity: 1;
}
86% {
opacity: 1;
}
100% {
inset-inline-start: 100%;
opacity: 0;
}
}
.svgb-10__ab {
display: grid;
gap: clamp(16px, 3vw, 28px);
grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr));
padding-block-start: 6px;
border-block-start: 1px solid var(--svgb-10-rest);
}
.svgb-10__abcell {
display: flex;
flex-direction: column;
gap: 8px;
}
.svgb-10__wide {
position: relative;
display: grid;
place-items: center;
block-size: 88px;
}
.svgb-10__vb {
position: absolute;
inset: 0;
inline-size: 100%;
block-size: 100%;
overflow: visible;
}
.svgb-10__vbline {
fill: none;
stroke: var(--svgb-10-stroke);
stroke-width: 2px;
}
.svgb-10__vbline--raw {
stroke: #c4341f;
stroke: oklch(0.53 0.19 32);
}
.svgb-10__wl {
position: relative;
font-family: 'JetBrains Mono', ui-monospace, monospace;
font-size: 12px;
}
.svgb-10__cap {
font-family: 'JetBrains Mono', ui-monospace, monospace;
font-size: 11px;
color: var(--svgb-10-muted);
}
.svgb-10__ti {
position: absolute;
opacity: 0;
pointer-events: none;
}
.svgb-10__toggle {
position: absolute;
z-index: 5;
inset-block-start: 16px;
inset-inline-end: 16px;
display: grid;
place-items: center;
inline-size: 44px;
block-size: 44px;
border-radius: 999px;
border: 1px solid color-mix(in oklab, currentColor 20%, transparent);
background: color-mix(in oklab, currentColor 7%, transparent);
color: inherit;
cursor: pointer;
transition: background 160ms ease, transform 160ms ease;
}
.svgb-10__toggle:hover {
background: color-mix(in oklab, currentColor 14%, transparent);
}
.svgb-10__toggle:active {
transform: scale(0.94);
}
.svgb-10__toggle svg {
inline-size: 18px;
block-size: 18px;
transition: transform 400ms cubic-bezier(0.2, 0.7, 0.3, 1);
}
.svgb-10__ti:checked + .svgb-10__toggle svg {
transform: rotate(180deg);
}
.svgb-10__ti:focus-visible + .svgb-10__toggle {
outline: 2px solid currentColor;
outline-offset: 3px;
}
.svgb-10__sr {
position: absolute;
inline-size: 1px;
block-size: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
.svgb-10:has(#svgb-10-theme:checked) {
--svgb-10-bg: #0f0e12;
--svgb-10-bg: oklch(0.18 0.006 290);
--svgb-10-fg: #f0eff4;
--svgb-10-fg: oklch(0.96 0.004 290);
--svgb-10-muted: #918f9c;
--svgb-10-muted: oklch(0.68 0.012 290);
--svgb-10-rest: rgba(240, 239, 244, 0.18);
--svgb-10-rest: oklch(0.96 0.004 290 / 0.18);
--svgb-10-stroke: #c4a8ff;
--svgb-10-stroke: oklch(0.82 0.15 295);
}
.svgb-10:has(#svgb-10-theme:checked) .svgb-10__vbline--raw {
stroke: oklch(0.75 0.16 32);
}
@media (prefers-reduced-motion: reduce) {
.svgb-10__frame--true {
transition: none;
}
.svgb-10__seam {
display: none;
}
.svgb-10__toggle,
.svgb-10__toggle svg {
transition: none;
}
}
```Here's a working CSS SVG Stroke Button 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: Non Scaling Stroke Button That Resizes with Text
Source: https://codefronts.com/components/css-svg-stroke-buttons/non-scaling-stroke-button-that-resizes-with-text/
The same call to action in six languages, right-to-left included, each button sized by its own text. Every frame is one fixed 200×56 viewBox stretched to whatever width the label needs, so the resting border is genuinely wrong — thin on the sides, heavy top and bottom. Hover does not draw a line: it wipes the corrected stroke across the button from the leading edge, so what you watch is a border becoming even rather than becoming present. Corners are square on purpose, which isolates the stroke error from the geometry error.
## HTML
```html
<section class="svgb-10">
<input class="svgb-10__ti" type="checkbox" id="svgb-10-theme">
<label class="svgb-10__toggle" for="svgb-10-theme">
<svg 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="svgb-10__sr">Switch colour scheme</span>
</label>
<div class="svgb-10__stage">
<div class="svgb-10__sheet">
<p class="svgb-10__kicker">One component · six locales · zero fixed widths</p>
<ul class="svgb-10__rows">
<li class="svgb-10__row">
<span class="svgb-10__loc">en</span>
<button class="svgb-10__btn" type="button">
<svg class="svgb-10__frame" viewBox="0 0 200 56" preserveAspectRatio="none" aria-hidden="true"><rect class="svgb-10__raw" x="1" y="1" width="198" height="54"/></svg>
<svg class="svgb-10__frame svgb-10__frame--true" viewBox="0 0 200 56" preserveAspectRatio="none" aria-hidden="true"><rect class="svgb-10__true" x="1" y="1" width="198" height="54" vector-effect="non-scaling-stroke"/></svg>
<span class="svgb-10__seam" aria-hidden="true"></span>
<span class="svgb-10__label">Start free trial</span>
</button>
</li>
<li class="svgb-10__row">
<span class="svgb-10__loc">de</span>
<button class="svgb-10__btn" type="button">
<svg class="svgb-10__frame" viewBox="0 0 200 56" preserveAspectRatio="none" aria-hidden="true"><rect class="svgb-10__raw" x="1" y="1" width="198" height="54"/></svg>
<svg class="svgb-10__frame svgb-10__frame--true" viewBox="0 0 200 56" preserveAspectRatio="none" aria-hidden="true"><rect class="svgb-10__true" x="1" y="1" width="198" height="54" vector-effect="non-scaling-stroke"/></svg>
<span class="svgb-10__seam" aria-hidden="true"></span>
<span class="svgb-10__label" lang="de">Kostenlos testen starten</span>
</button>
</li>
<li class="svgb-10__row">
<span class="svgb-10__loc">fi</span>
<button class="svgb-10__btn" type="button">
<svg class="svgb-10__frame" viewBox="0 0 200 56" preserveAspectRatio="none" aria-hidden="true"><rect class="svgb-10__raw" x="1" y="1" width="198" height="54"/></svg>
<svg class="svgb-10__frame svgb-10__frame--true" viewBox="0 0 200 56" preserveAspectRatio="none" aria-hidden="true"><rect class="svgb-10__true" x="1" y="1" width="198" height="54" vector-effect="non-scaling-stroke"/></svg>
<span class="svgb-10__seam" aria-hidden="true"></span>
<span class="svgb-10__label" lang="fi">Aloita maksuton kokeilujakso</span>
</button>
</li>
<li class="svgb-10__row">
<span class="svgb-10__loc">ja</span>
<button class="svgb-10__btn" type="button">
<svg class="svgb-10__frame" viewBox="0 0 200 56" preserveAspectRatio="none" aria-hidden="true"><rect class="svgb-10__raw" x="1" y="1" width="198" height="54"/></svg>
<svg class="svgb-10__frame svgb-10__frame--true" viewBox="0 0 200 56" preserveAspectRatio="none" aria-hidden="true"><rect class="svgb-10__true" x="1" y="1" width="198" height="54" vector-effect="non-scaling-stroke"/></svg>
<span class="svgb-10__seam" aria-hidden="true"></span>
<span class="svgb-10__label" lang="ja">無料デモ</span>
</button>
</li>
<li class="svgb-10__row" dir="rtl">
<span class="svgb-10__loc">ar</span>
<button class="svgb-10__btn" type="button">
<svg class="svgb-10__frame" viewBox="0 0 200 56" preserveAspectRatio="none" aria-hidden="true"><rect class="svgb-10__raw" x="1" y="1" width="198" height="54"/></svg>
<svg class="svgb-10__frame svgb-10__frame--true" viewBox="0 0 200 56" preserveAspectRatio="none" aria-hidden="true"><rect class="svgb-10__true" x="1" y="1" width="198" height="54" vector-effect="non-scaling-stroke"/></svg>
<span class="svgb-10__seam" aria-hidden="true"></span>
<span class="svgb-10__label" lang="ar">ابدأ النسخة المجانية</span>
</button>
</li>
<li class="svgb-10__row">
<span class="svgb-10__loc">live</span>
<button class="svgb-10__btn svgb-10__btn--edit" type="button">
<svg class="svgb-10__frame" viewBox="0 0 200 56" preserveAspectRatio="none" aria-hidden="true"><rect class="svgb-10__raw" x="1" y="1" width="198" height="54"/></svg>
<svg class="svgb-10__frame svgb-10__frame--true" viewBox="0 0 200 56" preserveAspectRatio="none" aria-hidden="true"><rect class="svgb-10__true" x="1" y="1" width="198" height="54" vector-effect="non-scaling-stroke"/></svg>
<span class="svgb-10__seam" aria-hidden="true"></span>
<span class="svgb-10__label" contenteditable="true" spellcheck="false">Type to resize the frame</span>
</button>
</li>
</ul>
<div class="svgb-10__ab">
<div class="svgb-10__abcell">
<div class="svgb-10__wide">
<svg class="svgb-10__vb" viewBox="0 0 200 56" preserveAspectRatio="none" aria-hidden="true">
<rect class="svgb-10__vbline" x="2" y="2" width="196" height="52" rx="14" vector-effect="non-scaling-stroke"/>
</svg>
<span class="svgb-10__wl">non-scaling-stroke</span>
</div>
<code class="svgb-10__cap">even 2px on all four edges</code>
</div>
<div class="svgb-10__abcell">
<div class="svgb-10__wide">
<svg class="svgb-10__vb" viewBox="0 0 200 56" preserveAspectRatio="none" aria-hidden="true">
<rect class="svgb-10__vbline svgb-10__vbline--raw" x="2" y="2" width="196" height="52" rx="14"/>
</svg>
<span class="svgb-10__wl">unprotected</span>
</div>
<code class="svgb-10__cap">uneven · the more-scaled axis paints thicker</code>
</div>
</div>
</div>
</div>
</section>
```
## CSS
```css
@import url('https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Inter+Tight:wght@400;500;600&family=JetBrains+Mono:wght@400&display=swap');
.svgb-10 {
--svgb-10-bg: #f7f5f0;
--svgb-10-bg: oklch(0.972 0.008 90);
--svgb-10-fg: #17161a;
--svgb-10-fg: oklch(0.20 0.006 290);
--svgb-10-muted: #6c6a75;
--svgb-10-muted: oklch(0.555 0.012 290);
--svgb-10-rest: rgba(23, 22, 26, 0.16);
--svgb-10-rest: oklch(0.20 0.006 290 / 0.16);
--svgb-10-stroke: #5b21f0;
--svgb-10-stroke: oklch(0.46 0.26 295);
--svgb-10-warn: #c4341f;
--svgb-10-warn: oklch(0.53 0.19 32);
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
background: var(--svgb-10-bg);
position: relative;
color: var(--svgb-10-fg);
font-family: 'Inter Tight', system-ui, sans-serif;
}
.svgb-10 *,
.svgb-10 *::before,
.svgb-10 *::after {
box-sizing: border-box;
}
.svgb-10__stage {
min-height: 100svh;
display: grid;
place-items: center;
padding: clamp(22px, 5vw, 68px);
}
.svgb-10__sheet {
width: 100%;
max-width: 880px;
display: flex;
flex-direction: column;
gap: clamp(20px, 3vw, 32px);
}
.svgb-10__kicker {
margin: 0;
font-family: 'Instrument Serif', ui-serif, Georgia, serif;
font-size: clamp(22px, 3.6vw, 38px);
font-style: italic;
letter-spacing: -0.02em;
line-height: 1.15;
}
.svgb-10__rows {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 14px;
}
.svgb-10__row {
display: flex;
align-items: center;
gap: 16px;
padding-block: 4px;
border-block-start: 1px solid var(--svgb-10-rest);
padding-block-start: 14px;
}
.svgb-10__loc {
flex: none;
inline-size: 42px;
font-family: 'JetBrains Mono', ui-monospace, monospace;
font-size: 10.5px;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--svgb-10-muted);
}
.svgb-10__btn {
position: relative;
display: inline-grid;
place-items: center;
min-inline-size: 92px;
min-block-size: 56px;
padding: 0 26px;
border: 0;
border-radius: 0;
background: transparent;
color: inherit;
font: inherit;
font-size: 15px;
font-weight: 500;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
}
.svgb-10__btn:focus-visible {
outline: 2px solid var(--svgb-10-stroke);
outline-offset: 3px;
}
.svgb-10__label {
position: relative;
z-index: 1;
outline: none;
}
.svgb-10__btn--edit .svgb-10__label {
min-inline-size: 4ch;
caret-color: var(--svgb-10-stroke);
}
.svgb-10__frame {
position: absolute;
inset: 0;
inline-size: 100%;
block-size: 100%;
overflow: visible;
}
.svgb-10__frame rect {
fill: none;
}
/* the resting edge is the UNPROTECTED stroke: one fixed viewBox stretched to the
button's width, so the sides thin out as the label grows */
.svgb-10__raw {
stroke: var(--svgb-10-rest);
stroke-width: 2px;
transition: stroke 240ms ease;
}
.svgb-10__btn:hover .svgb-10__raw,
.svgb-10__btn:focus-visible .svgb-10__raw {
stroke: color-mix(in oklab, var(--svgb-10-warn) 45%, transparent);
}
/* hover wipes the corrected stroke across it — a clip-path reveal, not a dash draw,
so what you watch is the line becoming even rather than becoming present */
.svgb-10__frame--true {
clip-path: inset(0 100% 0 0);
transition: clip-path 760ms cubic-bezier(0.22, 1, 0.36, 1);
}
.svgb-10__true {
stroke: var(--svgb-10-stroke);
stroke-width: 2px;
vector-effect: non-scaling-stroke;
}
/* inset() is physical, so RTL needs its own resting edge or the reveal fights the seam */
[dir="rtl"] .svgb-10__frame--true {
clip-path: inset(0 0 0 100%);
}
.svgb-10__btn:hover .svgb-10__frame--true,
.svgb-10__btn:focus-visible .svgb-10__frame--true {
clip-path: inset(0 0 0 0);
}
.svgb-10__seam {
position: absolute;
inset-block: -4px;
inline-size: 1.5px;
inset-inline-start: 0;
background: var(--svgb-10-stroke);
opacity: 0;
pointer-events: none;
}
.svgb-10__btn:hover .svgb-10__seam,
.svgb-10__btn:focus-visible .svgb-10__seam {
animation: svgb-10-seam 760ms cubic-bezier(0.22, 1, 0.36, 1) forwards;
}
@keyframes svgb-10-seam {
0% {
inset-inline-start: 0;
opacity: 0;
}
10% {
opacity: 1;
}
86% {
opacity: 1;
}
100% {
inset-inline-start: 100%;
opacity: 0;
}
}
.svgb-10__ab {
display: grid;
gap: clamp(16px, 3vw, 28px);
grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr));
padding-block-start: 6px;
border-block-start: 1px solid var(--svgb-10-rest);
}
.svgb-10__abcell {
display: flex;
flex-direction: column;
gap: 8px;
}
.svgb-10__wide {
position: relative;
display: grid;
place-items: center;
block-size: 88px;
}
.svgb-10__vb {
position: absolute;
inset: 0;
inline-size: 100%;
block-size: 100%;
overflow: visible;
}
.svgb-10__vbline {
fill: none;
stroke: var(--svgb-10-stroke);
stroke-width: 2px;
}
.svgb-10__vbline--raw {
stroke: #c4341f;
stroke: oklch(0.53 0.19 32);
}
.svgb-10__wl {
position: relative;
font-family: 'JetBrains Mono', ui-monospace, monospace;
font-size: 12px;
}
.svgb-10__cap {
font-family: 'JetBrains Mono', ui-monospace, monospace;
font-size: 11px;
color: var(--svgb-10-muted);
}
.svgb-10__ti {
position: absolute;
opacity: 0;
pointer-events: none;
}
.svgb-10__toggle {
position: absolute;
z-index: 5;
inset-block-start: 16px;
inset-inline-end: 16px;
display: grid;
place-items: center;
inline-size: 44px;
block-size: 44px;
border-radius: 999px;
border: 1px solid color-mix(in oklab, currentColor 20%, transparent);
background: color-mix(in oklab, currentColor 7%, transparent);
color: inherit;
cursor: pointer;
transition: background 160ms ease, transform 160ms ease;
}
.svgb-10__toggle:hover {
background: color-mix(in oklab, currentColor 14%, transparent);
}
.svgb-10__toggle:active {
transform: scale(0.94);
}
.svgb-10__toggle svg {
inline-size: 18px;
block-size: 18px;
transition: transform 400ms cubic-bezier(0.2, 0.7, 0.3, 1);
}
.svgb-10__ti:checked + .svgb-10__toggle svg {
transform: rotate(180deg);
}
.svgb-10__ti:focus-visible + .svgb-10__toggle {
outline: 2px solid currentColor;
outline-offset: 3px;
}
.svgb-10__sr {
position: absolute;
inline-size: 1px;
block-size: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
.svgb-10:has(#svgb-10-theme:checked) {
--svgb-10-bg: #0f0e12;
--svgb-10-bg: oklch(0.18 0.006 290);
--svgb-10-fg: #f0eff4;
--svgb-10-fg: oklch(0.96 0.004 290);
--svgb-10-muted: #918f9c;
--svgb-10-muted: oklch(0.68 0.012 290);
--svgb-10-rest: rgba(240, 239, 244, 0.18);
--svgb-10-rest: oklch(0.96 0.004 290 / 0.18);
--svgb-10-stroke: #c4a8ff;
--svgb-10-stroke: oklch(0.82 0.15 295);
}
.svgb-10:has(#svgb-10-theme:checked) .svgb-10__vbline--raw {
stroke: oklch(0.75 0.16 32);
}
@media (prefers-reduced-motion: reduce) {
.svgb-10__frame--true {
transition: none;
}
.svgb-10__seam {
display: none;
}
.svgb-10__toggle,
.svgb-10__toggle svg {
transition: none;
}
}
```How this works
The distortion is the subject, so the demo commits to it. A viewBox maps a coordinate system onto the element's box, and preserveAspectRatio="none" lets that mapping stretch unevenly. The direction that catches people is perpendicular: a stroke’s thickness is measured across the line, so the horizontal scale governs the vertical edges. A 200×56 box rendered at 420×56 is scaled 2.1× horizontally and 1× vertically, so a 2-unit stroke paints 4.2px on the sides and a true 2px on the top and bottom. These row buttons are exactly 56px tall, which pins the vertical scale at 1 — so the rails stay 2px in every row and the sides are the only thing that moves: 1.1px on the narrowest button, 2.4px on the widest. Whichever axis is scaled more paints the thicker edge, and the label length decides which one that is.
<!-- identical geometry, one attribute apart -->
<rect class="raw" x="1" y="1" width="198" height="54"/>
<rect class="true" x="1" y="1" width="198" height="54"
vector-effect="non-scaling-stroke"/>non-scaling-stroke opts the stroke out of the transform. The geometry still scales; only the pen does not. That is why the corrected layer is a true 2px on all four edges at any label length, and why it needs no per-width arithmetic.
The reveal is a wipe, not a draw. Both layers are always fully painted; the corrected one is simply clipped away. clip-path: inset(0 100% 0 0) hides it entirely, inset(0 0 0 0) shows it, and the transition between them sweeps the correction across the button. Because there is no stroke-dasharray anywhere, nothing has to know the perimeter — which matters here more than anywhere else in this set, since the perimeter changes with every translation.
.frame--true { clip-path: inset(0 100% 0 0);
transition: clip-path 760ms cubic-bezier(.22,1,.36,1); }
.btn:hover .frame--true { clip-path: inset(0 0 0 0); }Square corners are not a style choice. non-scaling-stroke fixes the pen, not the path: a stretched rx="14" corner becomes an ellipse whatever you do to the stroke. Keeping the corners square leaves stroke weight as the only variable, so the wipe demonstrates one thing. The A/B pair at the bottom keeps the rounded corners precisely to show that second, unfixable half of the problem.
Make it yours
- Reverse the wipe direction with
inset(0 0 0 100%)as the resting value — useful when the button sits at the right edge of a toolbar and the correction should arrive from the near side. - Wipe vertically (
inset(100% 0 0 0)) for a shorter, more mechanical read; horizontal suits wide buttons because the error itself is horizontal. - Drop the seam line for a quieter effect, or thicken it to 3px and tint it warm for a scanner-like pass.
- Keep the rounded corners and accept the ellipse if the radius is small relative to the stretch — below about 6 units of radius the distortion is hard to see.
- Swap the two layers to demonstrate the failure instead of the fix: wipe the unprotected stroke over the protected one for a before/after teaching slide.
Gotchas — read before shipping
- Stroke thickness is perpendicular to the line, so the horizontal scale distorts the side edges and the vertical scale distorts the top and bottom. Which pair looks wrong therefore flips with the box’s aspect ratio against the viewBox’s — there is no fixed answer, only ‘whichever axis is scaled more’.
vector-effect: non-scaling-strokeprotects the stroke width and nothing else. Corner radii, dash lengths and stroke-dasharray patterns all still scale with the viewBox, which is why a stretched rounded rect looks wrong even with the pen fixed.clip-pathon an<svg>element clips the whole layer including any overflow, so a stroke that bleeds outside the viewBox gets cut at the box edge. Give the layer room (or dropoverflow: visible) if the stroke sits on the boundary.- Two stacked layers means the resting stroke is painted underneath the corrected one. Keep the resting colour low-contrast, or the finished state reads as a doubled line.
vector-effectis an SVG presentation attribute that is also a CSS property, but only the attribute is universally supported on older engines — set it in the markup, as here, rather than only in the stylesheet.clip-pathhas no logical-property equivalent, so a wipe built frominset()does not followdirectionthe way the seam'sinset-inline-startdoes. Mixing the two silently sends the marker and the reveal in opposite directions under RTL — pair every physical clip with a[dir="rtl"]override, or keep both sides physical.- A percentage
inset()resolves against the element's own box, so the wipe duration feels different on a 96px button and a 420px one. If that inconsistency matters, scale the duration with the label length as demo 20 does.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 15.4+ | 113+ | 111+ |
SVG geometry properties as CSS: Chrome 64+, Firefox 71+, Safari 12.1+. vector-effect: non-scaling-stroke is SVG 1.1 and universal. pathLength is universal. oklch() and :has() set the stated floor.