SPB Git

spb/svgarden Public

SVGarden — searchable bank of 74 self-contained SVG+CSS animation snippets (svgarden.dev)

HTML 79.2% Astro 10.3% JavaScript 6% CSS 3.7% Shell 0.8%

feat(snippets): add buttons category (8)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Simon-Pierre Boucher committed 15 h ago (Aug 10, 2026) parent b3818b9

Showing 8 changed files with +768 and −0

added snippets/buttons/btn-border-draw.html +88 −0
@@ -0,0 +1,88 @@
1 +<!--
2 + ============================================================
3 + SVGarden — https://www.svgarden.dev
4 + Author : Simon-Pierre Boucher
5 + Contact: contact@spboucher.ai
6 + File : snippets/buttons/btn-border-draw.html
7 + Desc : Button whose border draws itself around the label on hover
8 + ============================================================
9 +-->
10 +<!--svgarden
11 +title: Border-draw button
12 +slug: btn-border-draw
13 +category: buttons
14 +tags: [button, border, draw, hover]
15 +difficulty: intermediate
16 +techniques: [pathLength, stroke-dashoffset, transition-reversal, letter-spacing]
17 +how_it_works: >
18 + A rect stretched over the button (preserveAspectRatio="none" + non-scaling
19 + stroke) is normalized to pathLength="100", so one dash of 100 hides the whole
20 + border. Hovering transitions stroke-dashoffset to 0 and the outline traces
21 + itself around the perimeter; because this is a transition rather than a
22 + keyframe animation, leaving the button plays the exact same motion backwards
23 + for free. A faint static track rect keeps the affordance visible at rest, and
24 + a small letter-spacing transition on the label makes the whole gesture feel
25 + intentional rather than decorative.
26 +customizable:
27 + - { var: "--sg-color", label: "Border", type: color, default: "#7F77DD" }
28 + - { var: "--sg-size", label: "Font size", type: range, min: 12, max: 26, default: 16, unit: px }
29 + - { var: "--sg-duration", label: "Speed", type: range, min: 0.2, max: 2, step: 0.1, default: 0.6, unit: s }
30 +created: 2026-08-10
31 +-->
32 +<button class="sg-btn-border-draw" type="button" style="--sg-color:#7F77DD; --sg-size:16px; --sg-duration:0.6s;">
33 + <svg viewBox="0 0 100 40" preserveAspectRatio="none" aria-hidden="true" focusable="false">
34 + <rect class="sg-btn-border-draw-track" x="1.5" y="1.5" width="97" height="37" rx="9" pathLength="100" />
35 + <rect class="sg-btn-border-draw-ink" x="1.5" y="1.5" width="97" height="37" rx="9" pathLength="100" />
36 + </svg>
37 + <span class="sg-btn-border-draw-label">Draw me in</span>
38 +</button>
39 +<style>
40 + .sg-btn-border-draw {
41 + position: relative;
42 + appearance: none;
43 + border: 0;
44 + background: transparent;
45 + color: inherit;
46 + font: inherit;
47 + font-size: var(--sg-size);
48 + font-weight: 600;
49 + padding: 0.75em 1.6em;
50 + cursor: pointer;
51 + }
52 + .sg-btn-border-draw svg {
53 + position: absolute;
54 + inset: 0;
55 + width: 100%;
56 + height: 100%;
57 + pointer-events: none;
58 + }
59 + .sg-btn-border-draw rect {
60 + /* no non-scaling-stroke here: Chromium computes dash patterns in screen
61 + space with it, which breaks the pathLength=100 normalization */
62 + fill: none;
63 + stroke: var(--sg-color);
64 + stroke-width: 2;
65 + stroke-linecap: round;
66 + }
67 + .sg-btn-border-draw-track {
68 + opacity: 0.25;
69 + }
70 + .sg-btn-border-draw-ink {
71 + /* gap 102 > pathLength so the round end-caps never bleed onto the path at rest */
72 + stroke-dasharray: 100 102;
73 + stroke-dashoffset: 101;
74 + transition: stroke-dashoffset var(--sg-duration) ease-in-out;
75 + }
76 + .sg-btn-border-draw-label {
77 + letter-spacing: 0.02em;
78 + transition: letter-spacing var(--sg-duration) ease;
79 + }
80 + .sg-btn-border-draw:hover .sg-btn-border-draw-ink,
81 + .sg-btn-border-draw:focus-visible .sg-btn-border-draw-ink {
82 + stroke-dashoffset: 0;
83 + }
84 + .sg-btn-border-draw:hover .sg-btn-border-draw-label,
85 + .sg-btn-border-draw:focus-visible .sg-btn-border-draw-label {
86 + letter-spacing: 0.09em;
87 + }
88 +</style>
added snippets/buttons/btn-elastic-press.html +76 −0
@@ -0,0 +1,76 @@
1 +<!--
2 + ============================================================
3 + SVGarden — https://www.svgarden.dev
4 + Author : Simon-Pierre Boucher
5 + Contact: contact@spboucher.ai
6 + File : snippets/buttons/btn-elastic-press.html
7 + Desc : Springy press feedback using CSS linear() easing — no JS
8 + ============================================================
9 +-->
10 +<!--svgarden
11 +title: Elastic press button
12 +slug: btn-elastic-press
13 +category: buttons
14 +tags: [button, spring, easing, press]
15 +difficulty: intermediate
16 +techniques: ["linear()-easing", squash-and-stretch, transition, "@supports"]
17 +how_it_works: >
18 + Real springs overshoot, rebound, and settle — motion a single cubic-bezier
19 + cannot describe because béziers only cross the final value once. CSS linear()
20 + fixes that by letting you plot the whole damped oscillation as a list of
21 + progress points (0, 1.28 at 35%, 0.92 at 60%, 1.04 at 80%, 1), which the
22 + browser connects into a piecewise curve. The press itself is a plain :active
23 + squash (scaleX up, scaleY down); the spring plays on release, when the
24 + transition back to scale(1) runs through the linear() curve. Inside the
25 + @supports guard we swap it in; older browsers keep a bouncy cubic-bezier that
26 + overshoots once — softer, but never broken.
27 +support: "linear() easing needs Chrome 113+, Safari 17.2+ or Firefox 112+; older browsers fall back to a single-overshoot cubic-bezier."
28 +customizable:
29 + - { var: "--sg-color", label: "Color", type: color, default: "#7F77DD" }
30 + - { var: "--sg-size", label: "Font size", type: range, min: 12, max: 28, default: 17, unit: px }
31 + - { var: "--sg-duration", label: "Spring time", type: range, min: 0.3, max: 1.6, step: 0.05, default: 0.7, unit: s }
32 +created: 2026-08-10
33 +-->
34 +<button class="sg-btn-elastic-press" type="button" style="--sg-color:#7F77DD; --sg-size:17px; --sg-duration:0.7s;">
35 + <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
36 + <path d="M12 3 L14.6 8.6 L20.5 9.3 L16.2 13.4 L17.4 19.4 L12 16.4 L6.6 19.4 L7.8 13.4 L3.5 9.3 L9.4 8.6 Z" />
37 + </svg>
38 + Press &amp; release
39 +</button>
40 +<style>
41 + .sg-btn-elastic-press {
42 + appearance: none;
43 + border: 0;
44 + font: inherit;
45 + font-size: var(--sg-size);
46 + font-weight: 600;
47 + color: #ffffff;
48 + background: var(--sg-color);
49 + padding: 0.75em 1.6em;
50 + border-radius: 14px;
51 + cursor: pointer;
52 + display: inline-flex;
53 + align-items: center;
54 + gap: 0.5em;
55 + transform: scale(1);
56 + transition: transform var(--sg-duration) cubic-bezier(0.3, 1.75, 0.45, 1);
57 + }
58 + .sg-btn-elastic-press svg {
59 + width: 1.1em;
60 + height: 1.1em;
61 + fill: #ffffff;
62 + opacity: 0.9;
63 + }
64 + .sg-btn-elastic-press:active {
65 + transform: scale(1.08, 0.82);
66 + transition-duration: 0.08s;
67 + transition-timing-function: ease-out;
68 + }
69 + @supports (transition-timing-function: linear(0, 1)) {
70 + .sg-btn-elastic-press {
71 + transition-timing-function: linear(
72 + 0, 0.62 18%, 1.28 35%, 0.92 60%, 1.04 80%, 0.99 92%, 1
73 + );
74 + }
75 + }
76 +</style>
added snippets/buttons/btn-goo-blob.html +95 −0
@@ -0,0 +1,95 @@
1 +<!--
2 + ============================================================
3 + SVGarden — https://www.svgarden.dev
4 + Author : Simon-Pierre Boucher
5 + Contact: contact@spboucher.ai
6 + File : snippets/buttons/btn-goo-blob.html
7 + Desc : Gooey blob splash bursting from behind the button on hover
8 + ============================================================
9 +-->
10 +<!--svgarden
11 +title: Goo blob button
12 +slug: btn-goo-blob
13 +category: buttons
14 +tags: [button, goo, filter, hover]
15 +difficulty: advanced
16 +techniques: [feGaussianBlur, feColorMatrix, goo-filter, transform-stagger]
17 +how_it_works: >
18 + The gooey look is a two-step SVG filter: feGaussianBlur smears the pill and
19 + its satellite circles into one soft cloud, then feColorMatrix multiplies the
20 + alpha channel by 19 and subtracts 9, which slams soft edges back to hard ones
21 + — but only where blurred shapes overlap enough, so nearby shapes appear to
22 + fuse like liquid. On hover the satellites simply translate outward; every
23 + merge and pinch-off you see is the matrix doing the work. The label lives in
24 + plain HTML above the filtered SVG, because running text through a goo filter
25 + turns it to mush.
26 +customizable:
27 + - { var: "--sg-color", label: "Goo color", type: color, default: "#7F77DD" }
28 + - { var: "--sg-size", label: "Width", type: range, min: 120, max: 240, default: 160, unit: px }
29 + - { var: "--sg-duration", label: "Speed", type: range, min: 0.2, max: 1.5, step: 0.05, default: 0.5, unit: s }
30 +created: 2026-08-10
31 +-->
32 +<button class="sg-btn-goo-blob" type="button" style="--sg-color:#7F77DD; --sg-size:160px; --sg-duration:0.5s;">
33 + <svg viewBox="0 0 160 80" aria-hidden="true" focusable="false">
34 + <defs>
35 + <filter id="sg-btn-goo-blob-goo" x="-30%" y="-60%" width="160%" height="220%">
36 + <feGaussianBlur in="SourceGraphic" stdDeviation="8" result="sg-btn-goo-blob-blur" />
37 + <feColorMatrix in="sg-btn-goo-blob-blur" mode="matrix"
38 + values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 19 -9" />
39 + </filter>
40 + </defs>
41 + <g class="sg-btn-goo-blob-goop">
42 + <rect x="20" y="24" width="120" height="32" rx="16" />
43 + <circle class="sg-btn-goo-blob-b1" cx="80" cy="40" r="9" />
44 + <circle class="sg-btn-goo-blob-b2" cx="80" cy="40" r="7" />
45 + <circle class="sg-btn-goo-blob-b3" cx="80" cy="40" r="8" />
46 + <circle class="sg-btn-goo-blob-b4" cx="80" cy="40" r="6" />
47 + </g>
48 + </svg>
49 + <span class="sg-btn-goo-blob-label">Hover the goo</span>
50 +</button>
51 +<style>
52 + .sg-btn-goo-blob {
53 + position: relative;
54 + appearance: none;
55 + border: 0;
56 + background: transparent;
57 + padding: 0;
58 + width: var(--sg-size);
59 + aspect-ratio: 2 / 1;
60 + cursor: pointer;
61 + font: inherit;
62 + }
63 + .sg-btn-goo-blob svg {
64 + position: absolute;
65 + inset: 0;
66 + width: 100%;
67 + height: 100%;
68 + overflow: visible;
69 + }
70 + .sg-btn-goo-blob-goop {
71 + fill: var(--sg-color);
72 + filter: url(#sg-btn-goo-blob-goo);
73 + }
74 + .sg-btn-goo-blob circle {
75 + transition: transform var(--sg-duration) cubic-bezier(0.3, 1.6, 0.5, 1);
76 + }
77 + .sg-btn-goo-blob-label {
78 + position: absolute;
79 + inset: 0;
80 + display: grid;
81 + place-items: center;
82 + color: #ffffff;
83 + font-weight: 600;
84 + font-size: calc(var(--sg-size) / 11);
85 + pointer-events: none;
86 + }
87 + .sg-btn-goo-blob:hover .sg-btn-goo-blob-b1,
88 + .sg-btn-goo-blob:focus-visible .sg-btn-goo-blob-b1 { transform: translate(-62px, -18px); }
89 + .sg-btn-goo-blob:hover .sg-btn-goo-blob-b2,
90 + .sg-btn-goo-blob:focus-visible .sg-btn-goo-blob-b2 { transform: translate(58px, -22px); }
91 + .sg-btn-goo-blob:hover .sg-btn-goo-blob-b3,
92 + .sg-btn-goo-blob:focus-visible .sg-btn-goo-blob-b3 { transform: translate(-48px, 20px); }
93 + .sg-btn-goo-blob:hover .sg-btn-goo-blob-b4,
94 + .sg-btn-goo-blob:focus-visible .sg-btn-goo-blob-b4 { transform: translate(52px, 24px); }
95 +</style>
added snippets/buttons/btn-liquid-swipe.html +101 −0
@@ -0,0 +1,101 @@
1 +<!--
2 + ============================================================
3 + SVGarden — https://www.svgarden.dev
4 + Author : Simon-Pierre Boucher
5 + Contact: contact@spboucher.ai
6 + File : snippets/buttons/btn-liquid-swipe.html
7 + Desc : Wavy liquid front sweeping across the button on hover
8 + ============================================================
9 +-->
10 +<!--svgarden
11 +title: Liquid swipe button
12 +slug: btn-liquid-swipe
13 +category: buttons
14 +tags: [button, liquid, clip-path, hover]
15 +difficulty: advanced
16 +techniques: [clipPath, animated-path-front, transition, preserveAspectRatio-none]
17 +how_it_works: >
18 + The "liquid" is one filled path shaped like a rectangle whose right edge
19 + undulates — that wavy edge is the liquid's front. A clipPath pins everything
20 + inside the button's rounded pill so the wave can slosh past the edges without
21 + spilling. At rest the path sits translated a full button-width left, in
22 + viewBox pixels because percentage translates are unreliable on SVG children;
23 + hovering transitions it to 0, and a low-amplitude keyframe animation bobs it vertically the
24 + whole time, which is what makes the front feel fluid instead of like a sliding
25 + card. The bob lives on a wrapper group rather than the path itself, because an
26 + animated transform on an SVG element replaces its static transform instead of
27 + composing with it — nesting sidesteps that.
28 +customizable:
29 + - { var: "--sg-color", label: "Liquid", type: color, default: "#4B9FE8" }
30 + - { var: "--sg-size", label: "Font size", type: range, min: 12, max: 26, default: 16, unit: px }
31 + - { var: "--sg-duration", label: "Speed", type: range, min: 0.3, max: 1.6, step: 0.05, default: 0.7, unit: s }
32 +created: 2026-08-10
33 +-->
34 +<button class="sg-btn-liquid-swipe" type="button" style="--sg-color:#4B9FE8; --sg-size:16px; --sg-duration:0.7s;">
35 + <svg viewBox="0 0 140 44" preserveAspectRatio="none" aria-hidden="true" focusable="false">
36 + <defs>
37 + <clipPath id="sg-btn-liquid-swipe-pill">
38 + <rect x="0" y="0" width="140" height="44" rx="22" />
39 + </clipPath>
40 + </defs>
41 + <rect class="sg-btn-liquid-swipe-rim" x="1" y="1" width="138" height="42" rx="21" />
42 + <g clip-path="url(#sg-btn-liquid-swipe-pill)">
43 + <g class="sg-btn-liquid-swipe-bob">
44 + <path class="sg-btn-liquid-swipe-wave"
45 + d="M-6 -8 H132 C 140 0, 128 8, 136 16 C 144 24, 128 32, 136 40 C 140 46, 134 50, 132 52 H-6 Z" />
46 + </g>
47 + </g>
48 + </svg>
49 + <span class="sg-btn-liquid-swipe-label">Dive in</span>
50 +</button>
51 +<style>
52 + .sg-btn-liquid-swipe {
53 + position: relative;
54 + appearance: none;
55 + border: 0;
56 + background: transparent;
57 + font: inherit;
58 + font-size: var(--sg-size);
59 + font-weight: 600;
60 + color: inherit;
61 + padding: 0.8em 2em;
62 + cursor: pointer;
63 + }
64 + .sg-btn-liquid-swipe svg {
65 + position: absolute;
66 + inset: 0;
67 + width: 100%;
68 + height: 100%;
69 + }
70 + .sg-btn-liquid-swipe-rim {
71 + fill: none;
72 + stroke: var(--sg-color);
73 + stroke-width: 2;
74 + vector-effect: non-scaling-stroke;
75 + }
76 + .sg-btn-liquid-swipe-wave {
77 + fill: var(--sg-color);
78 + /* viewBox units — percentage translate is unreliable on SVG children */
79 + transform: translateX(-152px);
80 + transition: transform var(--sg-duration) cubic-bezier(0.5, 0.1, 0.3, 1);
81 + }
82 + .sg-btn-liquid-swipe-bob {
83 + animation: sg-btn-liquid-swipe-bob 1.6s ease-in-out infinite;
84 + }
85 + .sg-btn-liquid-swipe-label {
86 + position: relative;
87 + transition: color calc(var(--sg-duration) * 0.7) ease;
88 + }
89 + .sg-btn-liquid-swipe:hover .sg-btn-liquid-swipe-wave,
90 + .sg-btn-liquid-swipe:focus-visible .sg-btn-liquid-swipe-wave {
91 + transform: translateX(0);
92 + }
93 + .sg-btn-liquid-swipe:hover .sg-btn-liquid-swipe-label,
94 + .sg-btn-liquid-swipe:focus-visible .sg-btn-liquid-swipe-label {
95 + color: #ffffff;
96 + }
97 + @keyframes sg-btn-liquid-swipe-bob {
98 + 0%, 100% { transform: translateY(0); }
99 + 50% { transform: translateY(-3px); }
100 + }
101 +</style>
added snippets/buttons/btn-magnetic-fill.html +91 −0
@@ -0,0 +1,91 @@
1 +<!--
2 + ============================================================
3 + SVGarden — https://www.svgarden.dev
4 + Author : Simon-Pierre Boucher
5 + Contact: contact@spboucher.ai
6 + File : snippets/buttons/btn-magnetic-fill.html
7 + Desc : Radial fill expanding from the cursor's entry side — zero JS
8 + ============================================================
9 +-->
10 +<!--svgarden
11 +title: Magnetic fill button
12 +slug: btn-magnetic-fill
13 +category: buttons
14 +tags: [button, fill, hover-zones, no-js]
15 +difficulty: advanced
16 +techniques: [sibling-combinator, hover-zones, transition, scale]
17 +how_it_works: >
18 + CSS cannot read cursor coordinates, so this button cheats honestly: three
19 + invisible strips cover its left, center and right thirds, and whichever strip
20 + the pointer touches first uses the sibling combinator (~) to reposition a
21 + shared ink circle before it scales up. Repositioning has no transition while
22 + the scale does, so the jump is invisible and the growth reads as "the fill
23 + started where I entered". It is a quantized approximation — three possible
24 + origins, not infinite — which is exactly the trade you make for needing zero
25 + JavaScript.
26 +customizable:
27 + - { var: "--sg-color", label: "Fill", type: color, default: "#7F77DD" }
28 + - { var: "--sg-size", label: "Font size", type: range, min: 12, max: 26, default: 16, unit: px }
29 + - { var: "--sg-duration", label: "Speed", type: range, min: 0.2, max: 1.2, step: 0.05, default: 0.45, unit: s }
30 +created: 2026-08-10
31 +-->
32 +<button class="sg-btn-magnetic-fill" type="button" style="--sg-color:#7F77DD; --sg-size:16px; --sg-duration:0.45s;">
33 + <span class="sg-btn-magnetic-fill-zone sg-btn-magnetic-fill-zone-l" aria-hidden="true"></span>
34 + <span class="sg-btn-magnetic-fill-zone sg-btn-magnetic-fill-zone-c" aria-hidden="true"></span>
35 + <span class="sg-btn-magnetic-fill-zone sg-btn-magnetic-fill-zone-r" aria-hidden="true"></span>
36 + <span class="sg-btn-magnetic-fill-ink" aria-hidden="true"></span>
37 + <span class="sg-btn-magnetic-fill-label">Magnetic fill</span>
38 +</button>
39 +<style>
40 + .sg-btn-magnetic-fill {
41 + position: relative;
42 + appearance: none;
43 + font: inherit;
44 + font-size: var(--sg-size);
45 + font-weight: 600;
46 + color: inherit;
47 + background: transparent;
48 + border: 1.5px solid var(--sg-color);
49 + border-radius: 999px;
50 + padding: 0.75em 1.8em;
51 + cursor: pointer;
52 + overflow: hidden;
53 + }
54 + .sg-btn-magnetic-fill-zone {
55 + position: absolute;
56 + top: 0;
57 + bottom: 0;
58 + width: 34%;
59 + z-index: 3;
60 + }
61 + .sg-btn-magnetic-fill-zone-l { left: 0; }
62 + .sg-btn-magnetic-fill-zone-c { left: 33%; }
63 + .sg-btn-magnetic-fill-zone-r { right: 0; }
64 + .sg-btn-magnetic-fill-ink {
65 + position: absolute;
66 + top: 50%;
67 + left: 50%;
68 + width: 1.5em;
69 + height: 1.5em;
70 + border-radius: 50%;
71 + background: var(--sg-color);
72 + transform: translate(-50%, -50%) scale(0);
73 + transition: transform var(--sg-duration) ease-out;
74 + z-index: 1;
75 + }
76 + .sg-btn-magnetic-fill-zone-l:hover ~ .sg-btn-magnetic-fill-ink { left: 12%; }
77 + .sg-btn-magnetic-fill-zone-r:hover ~ .sg-btn-magnetic-fill-ink { left: 88%; }
78 + .sg-btn-magnetic-fill:hover .sg-btn-magnetic-fill-ink,
79 + .sg-btn-magnetic-fill:focus-visible .sg-btn-magnetic-fill-ink {
80 + transform: translate(-50%, -50%) scale(10);
81 + }
82 + .sg-btn-magnetic-fill-label {
83 + position: relative;
84 + z-index: 2;
85 + transition: color var(--sg-duration) ease;
86 + }
87 + .sg-btn-magnetic-fill:hover .sg-btn-magnetic-fill-label,
88 + .sg-btn-magnetic-fill:focus-visible .sg-btn-magnetic-fill-label {
89 + color: #ffffff;
90 + }
91 +</style>
added snippets/buttons/btn-neon-trace.html +97 −0
@@ -0,0 +1,97 @@
1 +<!--
2 + ============================================================
3 + SVGarden — https://www.svgarden.dev
4 + Author : Simon-Pierre Boucher
5 + Contact: contact@spboucher.ai
6 + File : snippets/buttons/btn-neon-trace.html
7 + Desc : Neon light dash orbiting the button border with a hue sweep
8 + ============================================================
9 +-->
10 +<!--svgarden
11 +title: Neon trace button
12 +slug: btn-neon-trace
13 +category: buttons
14 +tags: [button, neon, dasharray, at-property]
15 +difficulty: advanced
16 +techniques: ["@property", stroke-dasharray-gap, hsl-calc, drop-shadow]
17 +how_it_works: >
18 + A short dash (stroke-dasharray "14 86" on a pathLength-100 rect) endlessly
19 + slides its dashoffset, so a single bright segment orbits the border. Its color
20 + is hsl(calc(var(--sg-neon-hue) * 1deg + var(--sg-btn-neon-trace-cycle)) …),
21 + where the cycle variable is registered with @property as a typed angle — that
22 + registration is what lets the keyframe animation interpolate it smoothly
23 + instead of snapping. Browsers without @property still animate the custom
24 + property, but discretely: the cycle runs 0deg→360deg, and since those are the
25 + same hue the snap is literally invisible — a degradation path chosen so the
26 + fallback needs no @supports at all.
27 +support: "@property is required for the smooth hue sweep (Chromium, Safari 16.4+, Firefox 128+); older browsers show the orbiting dash at a fixed hue."
28 +customizable:
29 + - { var: "--sg-neon-hue", label: "Base hue", type: range, min: 0, max: 360, default: 255 }
30 + - { var: "--sg-size", label: "Font size", type: range, min: 12, max: 26, default: 16, unit: px }
31 + - { var: "--sg-duration", label: "Orbit time", type: range, min: 0.8, max: 6, step: 0.1, default: 2.2, unit: s }
32 +created: 2026-08-10
33 +-->
34 +<button class="sg-btn-neon-trace" type="button" style="--sg-neon-hue:255; --sg-size:16px; --sg-duration:2.2s;">
35 + <svg viewBox="0 0 100 40" preserveAspectRatio="none" aria-hidden="true" focusable="false">
36 + <rect class="sg-btn-neon-trace-track" x="1.5" y="1.5" width="97" height="37" rx="9" pathLength="100" />
37 + <rect class="sg-btn-neon-trace-spark" x="1.5" y="1.5" width="97" height="37" rx="9" pathLength="100" />
38 + </svg>
39 + <span class="sg-btn-neon-trace-label">Neon trace</span>
40 +</button>
41 +<style>
42 + @property --sg-btn-neon-trace-cycle {
43 + syntax: '<angle>';
44 + inherits: false;
45 + initial-value: 0deg;
46 + }
47 + .sg-btn-neon-trace {
48 + --sg-btn-neon-trace-cycle: 0deg;
49 + position: relative;
50 + appearance: none;
51 + border: 0;
52 + background: transparent;
53 + font: inherit;
54 + font-size: var(--sg-size);
55 + font-weight: 600;
56 + color: inherit;
57 + padding: 0.8em 1.9em;
58 + cursor: pointer;
59 + animation: sg-btn-neon-trace-hue calc(var(--sg-duration) * 3) linear infinite;
60 + }
61 + .sg-btn-neon-trace svg {
62 + position: absolute;
63 + inset: 0;
64 + width: 100%;
65 + height: 100%;
66 + pointer-events: none;
67 + overflow: visible;
68 + }
69 + .sg-btn-neon-trace rect {
70 + /* no non-scaling-stroke: it makes Chromium measure dashes in screen space,
71 + desynchronizing the pathLength-based orbit */
72 + fill: none;
73 + stroke-width: 2;
74 + stroke-linecap: round;
75 + }
76 + .sg-btn-neon-trace-track {
77 + stroke: rgba(128, 128, 128, 0.35);
78 + }
79 + .sg-btn-neon-trace-spark {
80 + stroke: hsl(calc(var(--sg-neon-hue) * 1deg + var(--sg-btn-neon-trace-cycle)) 90% 62%);
81 + stroke-dasharray: 14 86;
82 + stroke-dashoffset: 0;
83 + filter: drop-shadow(0 0 4px hsl(calc(var(--sg-neon-hue) * 1deg + var(--sg-btn-neon-trace-cycle)) 90% 62%));
84 + animation: sg-btn-neon-trace-orbit var(--sg-duration) linear infinite;
85 + }
86 + .sg-btn-neon-trace:hover .sg-btn-neon-trace-spark,
87 + .sg-btn-neon-trace:focus-visible .sg-btn-neon-trace-spark {
88 + stroke-dasharray: 30 70;
89 + }
90 + @keyframes sg-btn-neon-trace-orbit {
91 + to { stroke-dashoffset: -100; }
92 + }
93 + @keyframes sg-btn-neon-trace-hue {
94 + from { --sg-btn-neon-trace-cycle: 0deg; }
95 + to { --sg-btn-neon-trace-cycle: 360deg; }
96 + }
97 +</style>
added snippets/buttons/btn-split-reveal.html +92 −0
@@ -0,0 +1,92 @@
1 +<!--
2 + ============================================================
3 + SVGarden — https://www.svgarden.dev
4 + Author : Simon-Pierre Boucher
5 + Contact: contact@spboucher.ai
6 + File : snippets/buttons/btn-split-reveal.html
7 + Desc : Label splits into two clipped halves revealing a second label
8 + ============================================================
9 +-->
10 +<!--svgarden
11 +title: Split-reveal button
12 +slug: btn-split-reveal
13 +category: buttons
14 +tags: [button, clip-path, split, hover]
15 +difficulty: intermediate
16 +techniques: ["clip-path: inset()", layered-duplicates, transition, transform]
17 +how_it_works: >
18 + The visible label is actually two identical copies stacked on top of each
19 + other: one clipped to its top half with clip-path: inset(0 0 50% 0), the
20 + other to its bottom half with inset(50% 0 0 0). On hover the halves translate
21 + in opposite vertical directions, so the word appears to tear apart along an
22 + invisible seam, while a third label scales up through the gap. Clipping is
23 + the load-bearing trick — both copies keep their full layout size, so nothing
24 + reflows during the animation and the split line always stays razor-straight.
25 +customizable:
26 + - { var: "--sg-color", label: "Accent", type: color, default: "#7F77DD" }
27 + - { var: "--sg-size", label: "Font size", type: range, min: 12, max: 26, default: 16, unit: px }
28 + - { var: "--sg-duration", label: "Speed", type: range, min: 0.15, max: 1.2, step: 0.05, default: 0.4, unit: s }
29 +created: 2026-08-10
30 +-->
31 +<button class="sg-btn-split-reveal" type="button" style="--sg-color:#7F77DD; --sg-size:16px; --sg-duration:0.4s;">
32 + <span class="sg-btn-split-reveal-base" aria-hidden="true">Hover me</span>
33 + <span class="sg-btn-split-reveal-half sg-btn-split-reveal-top" aria-hidden="true">Hover me</span>
34 + <span class="sg-btn-split-reveal-half sg-btn-split-reveal-bot" aria-hidden="true">Hover me</span>
35 + <span class="sg-btn-split-reveal-alt">Let's go →</span>
36 +</button>
37 +<style>
38 + .sg-btn-split-reveal {
39 + position: relative;
40 + appearance: none;
41 + font: inherit;
42 + font-size: var(--sg-size);
43 + font-weight: 600;
44 + color: inherit;
45 + background: transparent;
46 + border: 1.5px solid rgba(128, 128, 128, 0.45);
47 + border-radius: 10px;
48 + padding: 0.8em 1.9em;
49 + cursor: pointer;
50 + overflow: hidden;
51 + transition: border-color var(--sg-duration) ease;
52 + }
53 + .sg-btn-split-reveal-base {
54 + visibility: hidden;
55 + }
56 + .sg-btn-split-reveal-half {
57 + position: absolute;
58 + inset: 0;
59 + display: grid;
60 + place-items: center;
61 + transition: transform var(--sg-duration) cubic-bezier(0.5, 0, 0.2, 1);
62 + }
63 + .sg-btn-split-reveal-top { clip-path: inset(0 0 50% 0); }
64 + .sg-btn-split-reveal-bot { clip-path: inset(50% 0 0 0); }
65 + .sg-btn-split-reveal-alt {
66 + position: absolute;
67 + inset: 0;
68 + display: grid;
69 + place-items: center;
70 + color: var(--sg-color);
71 + transform: scale(0.6);
72 + opacity: 0;
73 + transition: transform var(--sg-duration) cubic-bezier(0.3, 1.5, 0.5, 1), opacity var(--sg-duration) ease;
74 + }
75 + .sg-btn-split-reveal:hover,
76 + .sg-btn-split-reveal:focus-visible {
77 + border-color: var(--sg-color);
78 + }
79 + .sg-btn-split-reveal:hover .sg-btn-split-reveal-top,
80 + .sg-btn-split-reveal:focus-visible .sg-btn-split-reveal-top {
81 + transform: translateY(-115%);
82 + }
83 + .sg-btn-split-reveal:hover .sg-btn-split-reveal-bot,
84 + .sg-btn-split-reveal:focus-visible .sg-btn-split-reveal-bot {
85 + transform: translateY(115%);
86 + }
87 + .sg-btn-split-reveal:hover .sg-btn-split-reveal-alt,
88 + .sg-btn-split-reveal:focus-visible .sg-btn-split-reveal-alt {
89 + transform: scale(1);
90 + opacity: 1;
91 + }
92 +</style>
added snippets/buttons/btn-success-morph.html +128 −0
@@ -0,0 +1,128 @@
1 +<!--
2 + ============================================================
3 + SVGarden — https://www.svgarden.dev
4 + Author : Simon-Pierre Boucher
5 + Contact: contact@spboucher.ai
6 + File : snippets/buttons/btn-success-morph.html
7 + Desc : Click morphs the button into a spinner, then a success check
8 + ============================================================
9 +-->
10 +<!--svgarden
11 +title: Success-morph button
12 +slug: btn-success-morph
13 +category: buttons
14 +tags: [button, state-machine, spinner, success, js]
15 +difficulty: advanced
16 +techniques: [data-state-attribute, width-morph, pathLength, animation-choreography]
17 +how_it_works: >
18 + The button is a three-state machine driven entirely by a data-state attribute
19 + — idle, loading, done — and the script's only job is to advance that attribute
20 + (with one setTimeout standing in for a real request). Every visual is CSS
21 + keyed off [data-state]: the pill's max-width collapses until it equals its
22 + height and the border-radius makes it a circle, the spinner arc fades in and
23 + spins, then the checkmark draws itself with the pathLength trick while the
24 + background settles green. Keeping state in an attribute instead of classes
25 + means the CSS reads like a truth table and impossible combinations simply
26 + cannot exist.
27 +customizable:
28 + - { var: "--sg-color", label: "Base color", type: color, default: "#7F77DD" }
29 + - { var: "--sg-success", label: "Success color", type: color, default: "#2BA05A" }
30 + - { var: "--sg-size", label: "Font size", type: range, min: 12, max: 24, default: 16, unit: px }
31 +created: 2026-08-10
32 +-->
33 +<button class="sg-btn-success-morph" type="button" data-state="idle" style="--sg-color:#7F77DD; --sg-success:#2BA05A; --sg-size:16px;">
34 + <span class="sg-btn-success-morph-label">Save changes</span>
35 + <svg viewBox="0 0 28 28" aria-hidden="true" focusable="false">
36 + <circle class="sg-btn-success-morph-arc" cx="14" cy="14" r="10" pathLength="100" />
37 + <path class="sg-btn-success-morph-check" d="M8 14.5 L12.5 19 L20 9.5" pathLength="100" />
38 + </svg>
39 +</button>
40 +<script>
41 + document.querySelectorAll('.sg-btn-success-morph:not([data-sg-init])').forEach(function (btn) {
42 + btn.setAttribute('data-sg-init', '');
43 + btn.addEventListener('click', function () {
44 + var state = btn.getAttribute('data-state');
45 + if (state === 'idle') {
46 + btn.setAttribute('data-state', 'loading');
47 + setTimeout(function () { btn.setAttribute('data-state', 'done'); }, 1400);
48 + } else if (state === 'done') {
49 + btn.setAttribute('data-state', 'idle');
50 + }
51 + });
52 + });
53 +</script>
54 +<style>
55 + .sg-btn-success-morph {
56 + appearance: none;
57 + border: 0;
58 + font: inherit;
59 + font-size: var(--sg-size);
60 + font-weight: 600;
61 + color: #ffffff;
62 + background: var(--sg-color);
63 + height: 3em;
64 + max-width: 12em;
65 + padding: 0 1.8em;
66 + border-radius: 1.5em;
67 + cursor: pointer;
68 + position: relative;
69 + overflow: hidden;
70 + white-space: nowrap;
71 + transition: max-width 0.45s cubic-bezier(0.6, 0, 0.3, 1), padding 0.45s ease, background 0.4s ease;
72 + }
73 + .sg-btn-success-morph-label {
74 + transition: opacity 0.2s ease;
75 + }
76 + .sg-btn-success-morph svg {
77 + position: absolute;
78 + inset: 0;
79 + margin: auto;
80 + width: 1.9em;
81 + height: 1.9em;
82 + }
83 + .sg-btn-success-morph-arc,
84 + .sg-btn-success-morph-check {
85 + fill: none;
86 + stroke: #ffffff;
87 + stroke-width: 2.5;
88 + stroke-linecap: round;
89 + stroke-linejoin: round;
90 + }
91 + .sg-btn-success-morph-arc {
92 + stroke-dasharray: 28 72;
93 + opacity: 0;
94 + transform-box: fill-box;
95 + transform-origin: center;
96 + }
97 + .sg-btn-success-morph-check {
98 + stroke-dasharray: 100;
99 + stroke-dashoffset: 100;
100 + }
101 + .sg-btn-success-morph[data-state='loading'] {
102 + max-width: 3em;
103 + padding: 0;
104 + cursor: progress;
105 + }
106 + .sg-btn-success-morph[data-state='loading'] .sg-btn-success-morph-label,
107 + .sg-btn-success-morph[data-state='done'] .sg-btn-success-morph-label {
108 + opacity: 0;
109 + }
110 + .sg-btn-success-morph[data-state='loading'] .sg-btn-success-morph-arc {
111 + opacity: 1;
112 + animation: sg-btn-success-morph-spin 0.8s linear infinite;
113 + }
114 + .sg-btn-success-morph[data-state='done'] {
115 + max-width: 3em;
116 + padding: 0;
117 + background: var(--sg-success);
118 + }
119 + .sg-btn-success-morph[data-state='done'] .sg-btn-success-morph-check {
120 + animation: sg-btn-success-morph-draw 0.45s ease-out 0.1s forwards;
121 + }
122 + @keyframes sg-btn-success-morph-spin {
123 + to { transform: rotate(360deg); }
124 + }
125 + @keyframes sg-btn-success-morph-draw {
126 + to { stroke-dashoffset: 0; }
127 + }
128 +</style>
129