feat(snippets): add buttons category (8)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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 & 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 | ||