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): 24 seed snippets across 7 categories (§7 list)

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

Showing 24 changed files with +1,663 and −0

added snippets/backgrounds/dots-drift.html +73 −0
@@ -0,0 +1,73 @@
1 +<!--
2 + ============================================================
3 + SVGarden — https://www.svgarden.dev
4 + Author : Simon-Pierre Boucher
5 + Contact: contact@spboucher.ai
6 + File : snippets/backgrounds/dots-drift.html
7 + Desc : Subtle drifting dot-grid pattern background
8 + ============================================================
9 +-->
10 +<!--svgarden
11 +title: Drifting dots
12 +slug: dots-drift
13 +category: backgrounds
14 +tags: [background, pattern, dots, ambient]
15 +difficulty: intermediate
16 +techniques: [svg-pattern, translate-property, seamless-tiling, layered-opacity]
17 +how_it_works: >
18 + A <pattern> tile of one dot fills two oversized rects. Because a pattern is
19 + periodic, translating a rect by exactly one tile (28 units) returns it to a
20 + pixel-identical state, so the infinite drift never shows a seam — the rects
21 + are drawn larger than the viewport to hide the moving edges. The two layers
22 + drift in opposite diagonals at different speeds, which adds quiet depth
23 + without ever pulling focus from the content above.
24 +customizable:
25 + - { var: "--sg-color", label: "Dot color", type: color, default: "#7F77DD" }
26 + - { var: "--sg-size", label: "Height", type: range, min: 80, max: 260, default: 150, unit: px }
27 + - { var: "--sg-duration", label: "Speed", type: range, min: 2, max: 20, step: 0.5, default: 8, unit: s }
28 +created: 2026-08-10
29 +-->
30 +<div class="sg-dots-drift" style="--sg-color:#7F77DD; --sg-size:150px; --sg-duration:8s;">
31 + <svg viewBox="0 0 280 140" preserveAspectRatio="xMidYMid slice" aria-hidden="true" focusable="false">
32 + <defs>
33 + <pattern id="sg-dots-drift-tile" width="28" height="28" patternUnits="userSpaceOnUse">
34 + <circle cx="4" cy="4" r="2.2" />
35 + </pattern>
36 + </defs>
37 + <rect class="sg-dots-drift-a" x="-28" y="-28" width="340" height="200" fill="url(#sg-dots-drift-tile)" />
38 + <rect class="sg-dots-drift-b" x="-28" y="-28" width="340" height="200" fill="url(#sg-dots-drift-tile)" />
39 + </svg>
40 +</div>
41 +<style>
42 + .sg-dots-drift {
43 + width: 100%;
44 + min-width: 240px;
45 + height: var(--sg-size);
46 + overflow: hidden;
47 + border-radius: 8px;
48 + }
49 + .sg-dots-drift svg {
50 + display: block;
51 + width: 100%;
52 + height: 100%;
53 + }
54 + .sg-dots-drift circle {
55 + fill: var(--sg-color);
56 + }
57 + .sg-dots-drift-a {
58 + opacity: 0.35;
59 + animation: sg-dots-drift-se var(--sg-duration) linear infinite;
60 + }
61 + .sg-dots-drift-b {
62 + opacity: 0.18;
63 + animation: sg-dots-drift-nw calc(var(--sg-duration) * 1.7) linear infinite;
64 + }
65 + @keyframes sg-dots-drift-se {
66 + from { translate: 0 0; }
67 + to { translate: 28px 28px; }
68 + }
69 + @keyframes sg-dots-drift-nw {
70 + from { translate: 14px 14px; }
71 + to { translate: -14px -14px; }
72 + }
73 +</style>
added snippets/backgrounds/wave-divider.html +67 −0
@@ -0,0 +1,67 @@
1 +<!--
2 + ============================================================
3 + SVGarden — https://www.svgarden.dev
4 + Author : Simon-Pierre Boucher
5 + Contact: contact@spboucher.ai
6 + File : snippets/backgrounds/wave-divider.html
7 + Desc : Animated layered wave section divider
8 + ============================================================
9 +-->
10 +<!--svgarden
11 +title: Wave divider
12 +slug: wave-divider
13 +category: backgrounds
14 +tags: [background, waves, divider, parallax]
15 +difficulty: advanced
16 +techniques: [periodic-paths, translate-property, layering, calc-durations]
17 +how_it_works: >
18 + Each wave path repeats the exact same 600-unit curve segment three times, so
19 + sliding it left by precisely one wavelength lands on an identical shape —
20 + that's the whole secret of a seamless loop. The three layers animate the
21 + modern `translate` property (leaving `transform` free for their static
22 + vertical offsets) at 1×, 1.5× and 2.2× the base duration, and the speed
23 + difference creates the parallax depth.
24 +customizable:
25 + - { var: "--sg-color", label: "Wave color", type: color, default: "#7F77DD" }
26 + - { var: "--sg-size", label: "Height", type: range, min: 60, max: 200, default: 110, unit: px }
27 + - { var: "--sg-duration", label: "Speed", type: range, min: 3, max: 16, step: 0.5, default: 7, unit: s }
28 +created: 2026-08-10
29 +-->
30 +<div class="sg-wave-divider" style="--sg-color:#7F77DD; --sg-size:110px; --sg-duration:7s;">
31 + <svg viewBox="0 0 1200 120" preserveAspectRatio="none" aria-hidden="true" focusable="false">
32 + <path d="M0 60 c150 -38 450 38 600 0 c150 -38 450 38 600 0 c150 -38 450 38 600 0 V120 H0 Z" />
33 + <path d="M0 60 c150 -38 450 38 600 0 c150 -38 450 38 600 0 c150 -38 450 38 600 0 V120 H0 Z" />
34 + <path d="M0 60 c150 -38 450 38 600 0 c150 -38 450 38 600 0 c150 -38 450 38 600 0 V120 H0 Z" />
35 + </svg>
36 +</div>
37 +<style>
38 + .sg-wave-divider {
39 + width: 100%;
40 + min-width: 260px;
41 + height: var(--sg-size);
42 + overflow: hidden;
43 + }
44 + .sg-wave-divider svg {
45 + display: block;
46 + width: 100%;
47 + height: 100%;
48 + }
49 + .sg-wave-divider path {
50 + fill: var(--sg-color);
51 + animation: sg-wave-divider-roll var(--sg-duration) linear infinite;
52 + }
53 + .sg-wave-divider path:nth-of-type(1) {
54 + opacity: 0.25;
55 + transform: translateY(-14px);
56 + animation-duration: calc(var(--sg-duration) * 2.2);
57 + }
58 + .sg-wave-divider path:nth-of-type(2) {
59 + opacity: 0.5;
60 + transform: translateY(-7px);
61 + animation-duration: calc(var(--sg-duration) * 1.5);
62 + }
63 + @keyframes sg-wave-divider-roll {
64 + from { translate: 0 0; }
65 + to { translate: -600px 0; }
66 + }
67 +</style>
added snippets/gauges/battery-fill.html +68 −0
@@ -0,0 +1,68 @@
1 +<!--
2 + ============================================================
3 + SVGarden — https://www.svgarden.dev
4 + Author : Simon-Pierre Boucher
5 + Contact: contact@spboucher.ai
6 + File : snippets/gauges/battery-fill.html
7 + Desc : Battery icon charging up to a target level
8 + ============================================================
9 +-->
10 +<!--svgarden
11 +title: Battery fill
12 +slug: battery-fill
13 +category: gauges
14 +tags: [gauge, battery, charging, scaleX]
15 +difficulty: intermediate
16 +techniques: [scaleX, calc-with-custom-properties, transform-origin-left, "@keyframes"]
17 +how_it_works: >
18 + The charge bar is a full-width rect scaled down horizontally: its resting
19 + state is scaleX(calc(var(--sg-level) / 100)), so the CSS variable directly is
20 + the battery percentage. The charging loop animates from scaleX(0) up to that
21 + same calculated value — because both the keyframe and the resting state read
22 + one variable, changing --sg-level retargets everything at once.
23 + transform-origin: left keeps the bar growing from the terminal end.
24 +customizable:
25 + - { var: "--sg-color", label: "Charge color", type: color, default: "#2BA05A" }
26 + - { var: "--sg-size", label: "Size", type: range, min: 60, max: 240, default: 110, unit: px }
27 + - { var: "--sg-level", label: "Level %", type: range, min: 5, max: 100, default: 80 }
28 +created: 2026-08-10
29 +-->
30 +<div class="sg-battery-fill" style="--sg-color:#2BA05A; --sg-size:110px; --sg-level:80;">
31 + <svg viewBox="0 0 56 28" role="img" aria-label="Battery charging">
32 + <title>Battery charging</title>
33 + <rect class="sg-battery-fill-case" x="2" y="4" width="46" height="20" rx="4" />
34 + <rect class="sg-battery-fill-cap" x="50" y="10" width="4" height="8" rx="1.5" />
35 + <rect class="sg-battery-fill-charge" x="5" y="7" width="40" height="14" rx="2" />
36 + </svg>
37 +</div>
38 +<style>
39 + .sg-battery-fill {
40 + width: var(--sg-size);
41 + }
42 + .sg-battery-fill svg {
43 + display: block;
44 + width: 100%;
45 + }
46 + .sg-battery-fill-case {
47 + fill: none;
48 + stroke: var(--sg-color);
49 + stroke-width: 2.5;
50 + opacity: 0.7;
51 + }
52 + .sg-battery-fill-cap {
53 + fill: var(--sg-color);
54 + opacity: 0.7;
55 + }
56 + .sg-battery-fill-charge {
57 + fill: var(--sg-color);
58 + transform-box: fill-box;
59 + transform-origin: left center;
60 + transform: scaleX(calc(var(--sg-level) / 100));
61 + animation: sg-battery-fill-charge-up 2.4s ease-out infinite;
62 + }
63 + @keyframes sg-battery-fill-charge-up {
64 + 0% { transform: scaleX(0); opacity: 0.6; }
65 + 75%, 88% { transform: scaleX(calc(var(--sg-level) / 100)); opacity: 1; }
66 + 100% { transform: scaleX(calc(var(--sg-level) / 100)); opacity: 0.6; }
67 + }
68 +</style>
added snippets/gauges/gauge-circle.html +93 −0
@@ -0,0 +1,93 @@
1 +<!--
2 + ============================================================
3 + SVGarden — https://www.svgarden.dev
4 + Author : Simon-Pierre Boucher
5 + Contact: contact@spboucher.ai
6 + File : snippets/gauges/gauge-circle.html
7 + Desc : Circular percentage gauge driven by a range slider (JS)
8 + ============================================================
9 +-->
10 +<!--svgarden
11 +title: Circle gauge
12 +slug: gauge-circle
13 +category: gauges
14 +tags: [gauge, percentage, slider, js]
15 +difficulty: intermediate
16 +techniques: [pathLength, stroke-dashoffset, css-transition, minimal-js]
17 +how_it_works: >
18 + With pathLength="100" the circle's circumference *is* 100, so showing v percent
19 + is just stroke-dashoffset = 100 − v — no trigonometry, no getTotalLength().
20 + The tiny script only copies the slider value onto that one CSS property and
21 + the text label; the smooth sweep between values comes for free from a CSS
22 + transition on stroke-dashoffset. The arc starts at 12 o'clock because the
23 + whole circle is rotated −90°.
24 +customizable:
25 + - { var: "--sg-color", label: "Color", type: color, default: "#7F77DD" }
26 + - { var: "--sg-size", label: "Size", type: range, min: 80, max: 220, default: 130, unit: px }
27 +created: 2026-08-10
28 +-->
29 +<div class="sg-gauge-circle" style="--sg-color:#7F77DD; --sg-size:130px;">
30 + <svg viewBox="0 0 100 100" role="img" aria-label="Percentage gauge">
31 + <title>Percentage gauge</title>
32 + <circle class="sg-gauge-circle-track" cx="50" cy="50" r="44" pathLength="100" />
33 + <circle class="sg-gauge-circle-arc" cx="50" cy="50" r="44" pathLength="100" />
34 + <text class="sg-gauge-circle-value" x="50" y="57">67%</text>
35 + </svg>
36 + <input class="sg-gauge-circle-slider" type="range" min="0" max="100" value="67" aria-label="Gauge value" />
37 +</div>
38 +<script>
39 + document.querySelectorAll('.sg-gauge-circle:not([data-sg-init])').forEach(function (root) {
40 + root.setAttribute('data-sg-init', '');
41 + var arc = root.querySelector('.sg-gauge-circle-arc');
42 + var label = root.querySelector('.sg-gauge-circle-value');
43 + var slider = root.querySelector('.sg-gauge-circle-slider');
44 + function render() {
45 + arc.style.strokeDashoffset = 100 - slider.value;
46 + label.textContent = slider.value + '%';
47 + }
48 + slider.addEventListener('input', render);
49 + render();
50 + });
51 +</script>
52 +<style>
53 + .sg-gauge-circle {
54 + width: var(--sg-size);
55 + display: flex;
56 + flex-direction: column;
57 + gap: 0.6rem;
58 + align-items: center;
59 + }
60 + .sg-gauge-circle svg {
61 + width: 100%;
62 + display: block;
63 + }
64 + .sg-gauge-circle-track,
65 + .sg-gauge-circle-arc {
66 + fill: none;
67 + stroke-width: 8;
68 + stroke-linecap: round;
69 + transform: rotate(-90deg);
70 + transform-origin: 50% 50%;
71 + }
72 + .sg-gauge-circle-track {
73 + stroke: var(--sg-color);
74 + opacity: 0.15;
75 + }
76 + .sg-gauge-circle-arc {
77 + stroke: var(--sg-color);
78 + stroke-dasharray: 100;
79 + stroke-dashoffset: 33;
80 + transition: stroke-dashoffset 0.45s ease;
81 + }
82 + .sg-gauge-circle-value {
83 + font-family: inherit;
84 + font-size: 20px;
85 + font-weight: 600;
86 + fill: currentColor;
87 + text-anchor: middle;
88 + }
89 + .sg-gauge-circle-slider {
90 + width: 90%;
91 + accent-color: var(--sg-color);
92 + }
93 +</style>
added snippets/gauges/gauge-semicircle.html +72 −0
@@ -0,0 +1,72 @@
1 +<!--
2 + ============================================================
3 + SVGarden — https://www.svgarden.dev
4 + Author : Simon-Pierre Boucher
5 + Contact: contact@spboucher.ai
6 + File : snippets/gauges/gauge-semicircle.html
7 + Desc : Speedometer-style semicircle gauge with a springy needle
8 + ============================================================
9 +-->
10 +<!--svgarden
11 +title: Semicircle speedometer
12 +slug: gauge-semicircle
13 +category: gauges
14 +tags: [gauge, speedometer, needle, css-only]
15 +difficulty: advanced
16 +techniques: [calc-with-custom-properties, dasharray-zones, rotate, cubic-bezier-overshoot]
17 +how_it_works: >
18 + The needle's resting angle is computed entirely in CSS: rotate(calc(var(--sg-value)
19 + * 1.8deg − 90deg)) maps 0–100 onto the −90°…+90° sweep of the dial. On load a
20 + keyframe animation swings it from −90° to that computed angle with an
21 + overshooting cubic-bezier, like a real spring-loaded needle. The colored zones
22 + are three copies of the same arc path wearing different stroke-dasharray
23 + windows — dash segments as a poor man's conic gradient.
24 +customizable:
25 + - { var: "--sg-color", label: "Needle", type: color, default: "#7F77DD" }
26 + - { var: "--sg-size", label: "Size", type: range, min: 100, max: 280, default: 170, unit: px }
27 + - { var: "--sg-value", label: "Value", type: range, min: 0, max: 100, default: 72 }
28 +created: 2026-08-10
29 +-->
30 +<div class="sg-gauge-semicircle" style="--sg-color:#7F77DD; --sg-size:170px; --sg-value:72;">
31 + <svg viewBox="0 0 100 62" role="img" aria-label="Speedometer gauge">
32 + <title>Speedometer gauge</title>
33 + <path class="sg-gauge-semicircle-zone1" d="M10 55 A40 40 0 0 1 90 55" pathLength="100" />
34 + <path class="sg-gauge-semicircle-zone2" d="M10 55 A40 40 0 0 1 90 55" pathLength="100" />
35 + <path class="sg-gauge-semicircle-zone3" d="M10 55 A40 40 0 0 1 90 55" pathLength="100" />
36 + <line class="sg-gauge-semicircle-needle" x1="50" y1="55" x2="50" y2="21" />
37 + <circle class="sg-gauge-semicircle-hub" cx="50" cy="55" r="4.5" />
38 + </svg>
39 +</div>
40 +<style>
41 + .sg-gauge-semicircle {
42 + width: var(--sg-size);
43 + }
44 + .sg-gauge-semicircle svg {
45 + display: block;
46 + width: 100%;
47 + }
48 + .sg-gauge-semicircle path {
49 + fill: none;
50 + stroke-width: 7;
51 + stroke-linecap: round;
52 + }
53 + .sg-gauge-semicircle-zone1 { stroke: #2ba05a; stroke-dasharray: 60 100; }
54 + .sg-gauge-semicircle-zone2 { stroke: #efbb33; stroke-dasharray: 0 60 25 100; }
55 + .sg-gauge-semicircle-zone3 { stroke: #d4553f; stroke-dasharray: 0 85 15 100; }
56 + .sg-gauge-semicircle-needle {
57 + stroke: var(--sg-color);
58 + stroke-width: 3;
59 + stroke-linecap: round;
60 + transform-box: view-box;
61 + transform-origin: 50px 55px;
62 + transform: rotate(calc(var(--sg-value) * 1.8deg - 90deg));
63 + animation: sg-gauge-semicircle-swing 1.2s cubic-bezier(0.3, 1.4, 0.5, 1);
64 + }
65 + .sg-gauge-semicircle-hub {
66 + fill: var(--sg-color);
67 + }
68 + @keyframes sg-gauge-semicircle-swing {
69 + from { transform: rotate(-90deg); }
70 + to { transform: rotate(calc(var(--sg-value) * 1.8deg - 90deg)); }
71 + }
72 +</style>
added snippets/hover/card-lift-border.html +77 −0
@@ -0,0 +1,77 @@
1 +<!--
2 + ============================================================
3 + SVGarden — https://www.svgarden.dev
4 + Author : Simon-Pierre Boucher
5 + Contact: contact@spboucher.ai
6 + File : snippets/hover/card-lift-border.html
7 + Desc : Card whose SVG border draws itself around it on hover
8 + ============================================================
9 +-->
10 +<!--svgarden
11 +title: Card lift & border
12 +slug: card-lift-border
13 +category: hover
14 +tags: [hover, card, border, draw]
15 +difficulty: intermediate
16 +techniques: [pathLength, stroke-dashoffset, transition-on-hover, preserveAspectRatio-none]
17 +how_it_works: >
18 + An SVG rect stretched over the card (preserveAspectRatio="none" + non-scaling
19 + stroke) starts fully hidden by its own dash gap. Hovering transitions
20 + stroke-dashoffset to zero, so the border traces itself around the perimeter —
21 + a transition, not a keyframe animation, which means it elegantly reverses
22 + when the pointer leaves instead of snapping. The card lift is a separate
23 + translateY transition on the wrapper.
24 +customizable:
25 + - { var: "--sg-color", label: "Border", type: color, default: "#7F77DD" }
26 + - { var: "--sg-size", label: "Width", type: range, min: 140, max: 360, default: 220, unit: px }
27 + - { var: "--sg-duration", label: "Speed", type: range, min: 0.2, max: 2, step: 0.1, default: 0.7, unit: s }
28 +created: 2026-08-10
29 +-->
30 +<div class="sg-card-lift-border" tabindex="0" style="--sg-color:#7F77DD; --sg-size:220px; --sg-duration:0.7s;">
31 + <svg viewBox="0 0 100 100" preserveAspectRatio="none" aria-hidden="true" focusable="false">
32 + <rect x="1" y="1" width="98" height="98" rx="6" pathLength="100" />
33 + </svg>
34 + <strong>Hover me</strong>
35 + <span>The border draws itself around the card, then un-draws on leave.</span>
36 +</div>
37 +<style>
38 + .sg-card-lift-border {
39 + position: relative;
40 + width: var(--sg-size);
41 + padding: 1.1rem 1.2rem;
42 + border-radius: 10px;
43 + background: rgba(127, 119, 221, 0.08);
44 + font-family: inherit;
45 + display: flex;
46 + flex-direction: column;
47 + gap: 0.4rem;
48 + cursor: pointer;
49 + transition: transform var(--sg-duration) ease;
50 + }
51 + .sg-card-lift-border strong { font-size: 1rem; }
52 + .sg-card-lift-border span { font-size: 0.85rem; opacity: 0.75; }
53 + .sg-card-lift-border svg {
54 + position: absolute;
55 + inset: 0;
56 + width: 100%;
57 + height: 100%;
58 + pointer-events: none;
59 + }
60 + .sg-card-lift-border rect {
61 + fill: none;
62 + stroke: var(--sg-color);
63 + stroke-width: 2;
64 + vector-effect: non-scaling-stroke;
65 + stroke-dasharray: 100;
66 + stroke-dashoffset: 100;
67 + transition: stroke-dashoffset var(--sg-duration) ease-in-out;
68 + }
69 + .sg-card-lift-border:hover,
70 + .sg-card-lift-border:focus-visible {
71 + transform: translateY(-4px);
72 + }
73 + .sg-card-lift-border:hover rect,
74 + .sg-card-lift-border:focus-visible rect {
75 + stroke-dashoffset: 0;
76 + }
77 +</style>
added snippets/hover/icon-morph-menu.html +75 −0
@@ -0,0 +1,75 @@
1 +<!--
2 + ============================================================
3 + SVGarden — https://www.svgarden.dev
4 + Author : Simon-Pierre Boucher
5 + Contact: contact@spboucher.ai
6 + File : snippets/hover/icon-morph-menu.html
7 + Desc : Hamburger menu morphing into an X on hover
8 + ============================================================
9 +-->
10 +<!--svgarden
11 +title: Menu ⇄ X morph
12 +slug: icon-morph-menu
13 +category: hover
14 +tags: [hover, menu, hamburger, morph]
15 +difficulty: intermediate
16 +techniques: [transition, translate-then-rotate, opacity, transform-origin]
17 +how_it_works: >
18 + Three <line> elements fake a path morph with plain transforms: on hover the
19 + top and bottom bars first translate to the vertical center, then rotate ±45°
20 + to form the X, while the middle bar fades and shrinks away. The two-step feel
21 + comes from transitioning translate and rotate with the same duration but
22 + letting the rotation visually dominate — order the transforms translate-first
23 + or the bars swing along arcs instead of collapsing cleanly.
24 +customizable:
25 + - { var: "--sg-color", label: "Color", type: color, default: "#7F77DD" }
26 + - { var: "--sg-size", label: "Size", type: range, min: 24, max: 96, default: 48, unit: px }
27 + - { var: "--sg-duration", label: "Speed", type: range, min: 0.1, max: 1, step: 0.05, default: 0.3, unit: s }
28 +created: 2026-08-10
29 +-->
30 +<button class="sg-icon-morph-menu" type="button" aria-label="Menu" style="--sg-color:#7F77DD; --sg-size:48px; --sg-duration:0.3s;">
31 + <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
32 + <line class="sg-icon-morph-menu-top" x1="4" y1="7" x2="20" y2="7" />
33 + <line class="sg-icon-morph-menu-mid" x1="4" y1="12" x2="20" y2="12" />
34 + <line class="sg-icon-morph-menu-bot" x1="4" y1="17" x2="20" y2="17" />
35 + </svg>
36 +</button>
37 +<style>
38 + .sg-icon-morph-menu {
39 + appearance: none;
40 + border: 1px solid var(--sg-color);
41 + border-radius: 10px;
42 + padding: 0;
43 + background: transparent;
44 + cursor: pointer;
45 + width: var(--sg-size);
46 + height: var(--sg-size);
47 + display: inline-grid;
48 + place-items: center;
49 + }
50 + .sg-icon-morph-menu svg {
51 + width: 70%;
52 + height: 70%;
53 + }
54 + .sg-icon-morph-menu line {
55 + stroke: var(--sg-color);
56 + stroke-width: 2.4;
57 + stroke-linecap: round;
58 + transform-box: fill-box;
59 + transform-origin: center;
60 + transition: transform var(--sg-duration) ease, opacity var(--sg-duration) ease;
61 + }
62 + .sg-icon-morph-menu:hover .sg-icon-morph-menu-top,
63 + .sg-icon-morph-menu:focus-visible .sg-icon-morph-menu-top {
64 + transform: translateY(5px) rotate(45deg);
65 + }
66 + .sg-icon-morph-menu:hover .sg-icon-morph-menu-bot,
67 + .sg-icon-morph-menu:focus-visible .sg-icon-morph-menu-bot {
68 + transform: translateY(-5px) rotate(-45deg);
69 + }
70 + .sg-icon-morph-menu:hover .sg-icon-morph-menu-mid,
71 + .sg-icon-morph-menu:focus-visible .sg-icon-morph-menu-mid {
72 + transform: scaleX(0.1);
73 + opacity: 0;
74 + }
75 +</style>
added snippets/hover/magnetic-arrow.html +73 −0
@@ -0,0 +1,73 @@
1 +<!--
2 + ============================================================
3 + SVGarden — https://www.svgarden.dev
4 + Author : Simon-Pierre Boucher
5 + Contact: contact@spboucher.ai
6 + File : snippets/hover/magnetic-arrow.html
7 + Desc : Link arrow that stretches forward on hover
8 + ============================================================
9 +-->
10 +<!--svgarden
11 +title: Magnetic arrow
12 +slug: magnetic-arrow
13 +category: hover
14 +tags: [hover, arrow, link, micro-interaction]
15 +difficulty: beginner
16 +techniques: [transition, translateX, scaleX, transform-origin-left]
17 +how_it_works: >
18 + The arrow is two parts with two different jobs: the shaft is a line that
19 + scaleX-stretches from its left edge (transform-origin: left keeps the tail
20 + pinned), while the head is a polyline that translateX-slides forward. Because
21 + both transitions share one duration and easing, they read as a single elastic
22 + gesture. A springy cubic-bezier with a y-value above 1 adds the magnetic
23 + overshoot on arrival.
24 +customizable:
25 + - { var: "--sg-color", label: "Color", type: color, default: "#7F77DD" }
26 + - { var: "--sg-size", label: "Size", type: range, min: 14, max: 32, default: 18, unit: px }
27 + - { var: "--sg-duration", label: "Speed", type: range, min: 0.1, max: 1, step: 0.05, default: 0.35, unit: s }
28 +created: 2026-08-10
29 +-->
30 +<a class="sg-magnetic-arrow" href="#sg-demo" style="--sg-color:#7F77DD; --sg-size:18px; --sg-duration:0.35s;">
31 + Read the story
32 + <svg viewBox="0 0 24 16" aria-hidden="true" focusable="false">
33 + <line class="sg-magnetic-arrow-shaft" x1="2" y1="8" x2="15" y2="8" />
34 + <polyline class="sg-magnetic-arrow-head" points="10,2.5 15.5,8 10,13.5" />
35 + </svg>
36 +</a>
37 +<style>
38 + .sg-magnetic-arrow {
39 + display: inline-flex;
40 + align-items: center;
41 + gap: 0.45em;
42 + font-size: var(--sg-size);
43 + font-weight: 600;
44 + color: var(--sg-color);
45 + text-decoration: none;
46 + }
47 + .sg-magnetic-arrow svg {
48 + width: 1.35em;
49 + height: 0.9em;
50 + overflow: visible;
51 + }
52 + .sg-magnetic-arrow line,
53 + .sg-magnetic-arrow polyline {
54 + fill: none;
55 + stroke: var(--sg-color);
56 + stroke-width: 2.2;
57 + stroke-linecap: round;
58 + stroke-linejoin: round;
59 + transition: transform var(--sg-duration) cubic-bezier(0.3, 1.5, 0.5, 1);
60 + }
61 + .sg-magnetic-arrow-shaft {
62 + transform-origin: left center;
63 + transform-box: fill-box;
64 + }
65 + .sg-magnetic-arrow:hover .sg-magnetic-arrow-shaft,
66 + .sg-magnetic-arrow:focus-visible .sg-magnetic-arrow-shaft {
67 + transform: scaleX(1.45);
68 + }
69 + .sg-magnetic-arrow:hover .sg-magnetic-arrow-head,
70 + .sg-magnetic-arrow:focus-visible .sg-magnetic-arrow-head {
71 + transform: translateX(6px);
72 + }
73 +</style>
added snippets/hover/star-spin.html +65 −0
@@ -0,0 +1,65 @@
1 +<!--
2 + ============================================================
3 + SVGarden — https://www.svgarden.dev
4 + Author : Simon-Pierre Boucher
5 + Contact: contact@spboucher.ai
6 + File : snippets/hover/star-spin.html
7 + Desc : Star that spins and scales up on hover
8 + ============================================================
9 +-->
10 +<!--svgarden
11 +title: Star spin
12 +slug: star-spin
13 +category: hover
14 +tags: [hover, star, rotate, transition]
15 +difficulty: beginner
16 +techniques: [transition, transform-box, rotate, scale, focus-visible]
17 +how_it_works: >
18 + No keyframes here — just a transition on transform. Hovering (or keyboard-
19 + focusing) the button rotates the polygon 72°, exactly one point of a
20 + five-pointed star, so it lands looking identical to where it started while
21 + clearly having moved. transform-box: fill-box makes the star rotate around
22 + its own center; without it, SVG children rotate around the top-left of the
23 + viewport and fly off in an arc.
24 +customizable:
25 + - { var: "--sg-color", label: "Color", type: color, default: "#EFBB33" }
26 + - { var: "--sg-size", label: "Size", type: range, min: 24, max: 120, default: 56, unit: px }
27 + - { var: "--sg-duration", label: "Speed", type: range, min: 0.1, max: 1.5, step: 0.05, default: 0.4, unit: s }
28 +created: 2026-08-10
29 +-->
30 +<button class="sg-star-spin" type="button" aria-label="Favorite" style="--sg-color:#EFBB33; --sg-size:56px; --sg-duration:0.4s;">
31 + <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
32 + <polygon points="12,2.5 14.9,9 22,9.7 16.7,14.5 18.2,21.5 12,17.8 5.8,21.5 7.3,14.5 2,9.7 9.1,9" />
33 + </svg>
34 +</button>
35 +<style>
36 + .sg-star-spin {
37 + appearance: none;
38 + border: 0;
39 + padding: 0;
40 + background: transparent;
41 + cursor: pointer;
42 + width: var(--sg-size);
43 + height: var(--sg-size);
44 + display: inline-grid;
45 + place-items: center;
46 + }
47 + .sg-star-spin svg {
48 + width: 100%;
49 + height: 100%;
50 + overflow: visible;
51 + }
52 + .sg-star-spin polygon {
53 + fill: var(--sg-color);
54 + stroke: var(--sg-color);
55 + stroke-width: 1;
56 + stroke-linejoin: round;
57 + transform-box: fill-box;
58 + transform-origin: center;
59 + transition: transform var(--sg-duration) cubic-bezier(0.35, 1.6, 0.6, 1);
60 + }
61 + .sg-star-spin:hover polygon,
62 + .sg-star-spin:focus-visible polygon {
63 + transform: rotate(72deg) scale(1.18);
64 + }
65 +</style>
added snippets/loaders/bar-indeterminate.html +58 −0
@@ -0,0 +1,58 @@
1 +<!--
2 + ============================================================
3 + SVGarden — https://www.svgarden.dev
4 + Author : Simon-Pierre Boucher
5 + Contact: contact@spboucher.ai
6 + File : snippets/loaders/bar-indeterminate.html
7 + Desc : Sliding indeterminate progress bar
8 + ============================================================
9 +-->
10 +<!--svgarden
11 +title: Indeterminate bar
12 +slug: bar-indeterminate
13 +category: loaders
14 +tags: [loader, progress, bar, keyframes]
15 +difficulty: beginner
16 +techniques: [translate, scaleX, "@keyframes", rx-rounded-rect]
17 +how_it_works: >
18 + A rounded track rect sits under a shorter "runner" rect. The runner slides
19 + from off-screen left to off-screen right with translateX while scaleX
20 + stretches it mid-flight and squashes it at both ends — that squash/stretch
21 + is what sells the material-style indeterminate feel rather than a rigid
22 + block gliding by. ease-in-out on an infinite loop gives it a breathing rhythm.
23 +customizable:
24 + - { var: "--sg-color", label: "Color", type: color, default: "#7F77DD" }
25 + - { var: "--sg-size", label: "Width", type: range, min: 100, max: 400, default: 200, unit: px }
26 + - { var: "--sg-duration", label: "Speed", type: range, min: 0.6, max: 4, step: 0.1, default: 1.4, unit: s }
27 +created: 2026-08-10
28 +-->
29 +<div class="sg-bar-indeterminate" style="--sg-color:#7F77DD; --sg-size:200px; --sg-duration:1.4s;">
30 + <svg viewBox="0 0 120 8" aria-hidden="true" focusable="false">
31 + <rect class="sg-bar-indeterminate-track" x="0" y="1" width="120" height="6" rx="3" />
32 + <rect class="sg-bar-indeterminate-runner" x="0" y="1" width="36" height="6" rx="3" />
33 + </svg>
34 +</div>
35 +<style>
36 + .sg-bar-indeterminate {
37 + width: var(--sg-size);
38 + }
39 + .sg-bar-indeterminate svg {
40 + display: block;
41 + width: 100%;
42 + }
43 + .sg-bar-indeterminate-track {
44 + fill: var(--sg-color);
45 + opacity: 0.18;
46 + }
47 + .sg-bar-indeterminate-runner {
48 + fill: var(--sg-color);
49 + transform-box: fill-box;
50 + transform-origin: center;
51 + animation: sg-bar-indeterminate-slide var(--sg-duration) ease-in-out infinite;
52 + }
53 + @keyframes sg-bar-indeterminate-slide {
54 + 0% { transform: translateX(-40px) scaleX(0.5); }
55 + 50% { transform: translateX(42px) scaleX(1.6); }
56 + 100% { transform: translateX(126px) scaleX(0.5); }
57 + }
58 +</style>
added snippets/loaders/dots-pulse.html +56 −0
@@ -0,0 +1,56 @@
1 +<!--
2 + ============================================================
3 + SVGarden — https://www.svgarden.dev
4 + Author : Simon-Pierre Boucher
5 + Contact: contact@spboucher.ai
6 + File : snippets/loaders/dots-pulse.html
7 + Desc : Three SVG circles pulsing in sequence
8 + ============================================================
9 +-->
10 +<!--svgarden
11 +title: Pulsing dots
12 +slug: dots-pulse
13 +category: loaders
14 +tags: [loader, dots, stagger, keyframes]
15 +difficulty: beginner
16 +techniques: [animation-delay, transform-box, scale, opacity]
17 +how_it_works: >
18 + All three circles share one shrink-and-fade keyframe animation; the wave
19 + illusion is pure timing — each dot starts a third of the duration later
20 + than its neighbor via animation-delay. transform-box: fill-box makes each
21 + circle scale around its own center instead of the SVG's origin, which is
22 + the classic gotcha when transforming SVG elements with CSS.
23 +customizable:
24 + - { var: "--sg-color", label: "Color", type: color, default: "#7F77DD" }
25 + - { var: "--sg-size", label: "Size", type: range, min: 32, max: 160, default: 64, unit: px }
26 + - { var: "--sg-duration", label: "Speed", type: range, min: 0.4, max: 3, step: 0.1, default: 1.2, unit: s }
27 +created: 2026-08-10
28 +-->
29 +<div class="sg-dots-pulse" style="--sg-color:#7F77DD; --sg-size:64px; --sg-duration:1.2s;">
30 + <svg viewBox="0 0 60 20" aria-hidden="true" focusable="false">
31 + <circle cx="10" cy="10" r="6" />
32 + <circle cx="30" cy="10" r="6" />
33 + <circle cx="50" cy="10" r="6" />
34 + </svg>
35 +</div>
36 +<style>
37 + .sg-dots-pulse {
38 + width: var(--sg-size);
39 + }
40 + .sg-dots-pulse svg {
41 + display: block;
42 + width: 100%;
43 + }
44 + .sg-dots-pulse circle {
45 + fill: var(--sg-color);
46 + transform-box: fill-box;
47 + transform-origin: center;
48 + animation: sg-dots-pulse-beat var(--sg-duration) ease-in-out infinite;
49 + }
50 + .sg-dots-pulse circle:nth-of-type(2) { animation-delay: calc(var(--sg-duration) / 3); }
51 + .sg-dots-pulse circle:nth-of-type(3) { animation-delay: calc(var(--sg-duration) / 3 * 2); }
52 + @keyframes sg-dots-pulse-beat {
53 + 0%, 100% { transform: scale(1); opacity: 1; }
54 + 50% { transform: scale(0.45); opacity: 0.35; }
55 + }
56 +</style>
added snippets/loaders/hourglass-flip.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/loaders/hourglass-flip.html
7 + Desc : Hourglass draining its sand, then flipping over
8 + ============================================================
9 +-->
10 +<!--svgarden
11 +title: Hourglass flip
12 +slug: hourglass-flip
13 +category: loaders
14 +tags: [loader, hourglass, flip, multi-animation]
15 +difficulty: advanced
16 +techniques: ["@keyframes", scaleY, rotate, stroke-dasharray, animation-choreography]
17 +how_it_works: >
18 + Three animations share one timeline: the top sand scaleY-shrinks toward the
19 + neck, the bottom pile grows from the floor, and a dashed stream trickles
20 + between them for the first 80% of the cycle. In the final 20% the whole SVG
21 + rotates 180°. Because the drained hourglass is the mirror of the full one,
22 + the rotation lands exactly on the first frame and the infinite loop is
23 + seamless — choreography, not trickery.
24 +customizable:
25 + - { var: "--sg-color", label: "Color", type: color, default: "#7F77DD" }
26 + - { var: "--sg-size", label: "Size", type: range, min: 32, max: 140, default: 56, unit: px }
27 + - { var: "--sg-duration", label: "Speed", type: range, min: 1.5, max: 8, step: 0.5, default: 3, unit: s }
28 +created: 2026-08-10
29 +-->
30 +<div class="sg-hourglass-flip" style="--sg-color:#7F77DD; --sg-size:56px; --sg-duration:3s;">
31 + <svg viewBox="0 0 40 56" aria-hidden="true" focusable="false">
32 + <path class="sg-hourglass-flip-frame" d="M9 5 H31 L22 28 L31 51 H9 L18 28 Z" />
33 + <path class="sg-hourglass-flip-top" d="M13 11 L27 11 L20 26 Z" />
34 + <path class="sg-hourglass-flip-bottom" d="M13 48 L27 48 L20 34 Z" />
35 + <line class="sg-hourglass-flip-stream" x1="20" y1="28" x2="20" y2="46" />
36 + </svg>
37 +</div>
38 +<style>
39 + .sg-hourglass-flip {
40 + width: var(--sg-size);
41 + }
42 + .sg-hourglass-flip svg {
43 + display: block;
44 + width: 100%;
45 + transform-origin: 50% 50%;
46 + animation: sg-hourglass-flip-turn var(--sg-duration) ease-in-out infinite;
47 + }
48 + .sg-hourglass-flip-frame {
49 + fill: none;
50 + stroke: var(--sg-color);
51 + stroke-width: 2.5;
52 + stroke-linejoin: round;
53 + }
54 + .sg-hourglass-flip-top,
55 + .sg-hourglass-flip-bottom {
56 + fill: var(--sg-color);
57 + transform-box: fill-box;
58 + animation: sg-hourglass-flip-drain var(--sg-duration) linear infinite;
59 + }
60 + .sg-hourglass-flip-top { transform-origin: center bottom; }
61 + .sg-hourglass-flip-bottom {
62 + transform-origin: center bottom;
63 + animation-name: sg-hourglass-flip-pile;
64 + }
65 + .sg-hourglass-flip-stream {
66 + stroke: var(--sg-color);
67 + stroke-width: 2;
68 + stroke-dasharray: 3 4;
69 + animation: sg-hourglass-flip-trickle var(--sg-duration) linear infinite;
70 + }
71 + @keyframes sg-hourglass-flip-turn {
72 + 0%, 80% { transform: rotate(0deg); }
73 + 100% { transform: rotate(180deg); }
74 + }
75 + @keyframes sg-hourglass-flip-drain {
76 + 0% { transform: scaleY(1); }
77 + 75%, 100% { transform: scaleY(0); }
78 + }
79 + @keyframes sg-hourglass-flip-pile {
80 + 0% { transform: scaleY(0); }
81 + 75%, 100% { transform: scaleY(1); }
82 + }
83 + @keyframes sg-hourglass-flip-trickle {
84 + 0% { stroke-dashoffset: 28; opacity: 1; }
85 + 75% { stroke-dashoffset: 0; opacity: 1; }
86 + 78%, 100% { opacity: 0; }
87 + }
88 +</style>
added snippets/loaders/orbit-dots.html +62 −0
@@ -0,0 +1,62 @@
1 +<!--
2 + ============================================================
3 + SVGarden — https://www.svgarden.dev
4 + Author : Simon-Pierre Boucher
5 + Contact: contact@spboucher.ai
6 + File : snippets/loaders/orbit-dots.html
7 + Desc : Dots orbiting a center point at nested speeds
8 + ============================================================
9 +-->
10 +<!--svgarden
11 +title: Orbit dots
12 +slug: orbit-dots
13 +category: loaders
14 +tags: [loader, orbit, rotate, nested-animation]
15 +difficulty: intermediate
16 +techniques: [transform-origin, rotate, calc-durations, svg-groups]
17 +how_it_works: >
18 + Each dot lives in its own <g> group that spins around the SVG center —
19 + the dot is simply drawn off-center, so rotating the group makes it orbit.
20 + All groups share one rotation keyframe; giving each a duration derived
21 + from the base speed with calc() (1×, 1.5×, 2×) means the dots drift in
22 + and out of alignment and only sync up once per full cycle.
23 +customizable:
24 + - { var: "--sg-color", label: "Color", type: color, default: "#7F77DD" }
25 + - { var: "--sg-size", label: "Size", type: range, min: 32, max: 140, default: 64, unit: px }
26 + - { var: "--sg-duration", label: "Speed", type: range, min: 0.8, max: 5, step: 0.1, default: 2, unit: s }
27 +created: 2026-08-10
28 +-->
29 +<div class="sg-orbit-dots" style="--sg-color:#7F77DD; --sg-size:64px; --sg-duration:2s;">
30 + <svg viewBox="0 0 60 60" aria-hidden="true" focusable="false">
31 + <circle class="sg-orbit-dots-core" cx="30" cy="30" r="4" />
32 + <g><circle cx="30" cy="8" r="4" /></g>
33 + <g><circle cx="30" cy="13" r="3" /></g>
34 + <g><circle cx="30" cy="18" r="2" /></g>
35 + </svg>
36 +</div>
37 +<style>
38 + .sg-orbit-dots {
39 + width: var(--sg-size);
40 + height: var(--sg-size);
41 + }
42 + .sg-orbit-dots svg {
43 + display: block;
44 + width: 100%;
45 + height: 100%;
46 + }
47 + .sg-orbit-dots circle {
48 + fill: var(--sg-color);
49 + }
50 + .sg-orbit-dots-core {
51 + opacity: 0.4;
52 + }
53 + .sg-orbit-dots g {
54 + transform-origin: 50% 50%;
55 + animation: sg-orbit-dots-turn var(--sg-duration) linear infinite;
56 + }
57 + .sg-orbit-dots g:nth-of-type(2) { animation-duration: calc(var(--sg-duration) * 1.5); opacity: 0.75; }
58 + .sg-orbit-dots g:nth-of-type(3) { animation-duration: calc(var(--sg-duration) * 2); opacity: 0.5; }
59 + @keyframes sg-orbit-dots-turn {
60 + 100% { transform: rotate(360deg); }
61 + }
62 +</style>
added snippets/loaders/ring-dual.html +61 −0
@@ -0,0 +1,61 @@
1 +<!--
2 + ============================================================
3 + SVGarden — https://www.svgarden.dev
4 + Author : Simon-Pierre Boucher
5 + Contact: contact@spboucher.ai
6 + File : snippets/loaders/ring-dual.html
7 + Desc : Two counter-rotating arcs forming a dual-ring loader
8 + ============================================================
9 +-->
10 +<!--svgarden
11 +title: Dual ring
12 +slug: ring-dual
13 +category: loaders
14 +tags: [loader, ring, counter-rotate, dasharray]
15 +difficulty: beginner
16 +techniques: [stroke-dasharray, animation-direction, transform-origin, "@keyframes"]
17 +how_it_works: >
18 + Each ring is a full circle whose stroke-dasharray leaves most of it blank,
19 + so only an arc is visible. Both arcs run the same rotation keyframes, but
20 + the inner one sets animation-direction: reverse and a shorter duration, so
21 + the two chase each other in opposite directions at different speeds —
22 + a lot of perceived complexity from one keyframe block.
23 +customizable:
24 + - { var: "--sg-color", label: "Color", type: color, default: "#7F77DD" }
25 + - { var: "--sg-size", label: "Size", type: range, min: 24, max: 120, default: 56, unit: px }
26 + - { var: "--sg-duration", label: "Speed", type: range, min: 0.5, max: 4, step: 0.1, default: 1.6, unit: s }
27 +created: 2026-08-10
28 +-->
29 +<div class="sg-ring-dual" style="--sg-color:#7F77DD; --sg-size:56px; --sg-duration:1.6s;">
30 + <svg viewBox="0 0 50 50" aria-hidden="true" focusable="false">
31 + <circle class="sg-ring-dual-outer" cx="25" cy="25" r="21" stroke-dasharray="66 132" />
32 + <circle class="sg-ring-dual-inner" cx="25" cy="25" r="13" stroke-dasharray="41 82" />
33 + </svg>
34 +</div>
35 +<style>
36 + .sg-ring-dual {
37 + width: var(--sg-size);
38 + height: var(--sg-size);
39 + }
40 + .sg-ring-dual svg {
41 + display: block;
42 + width: 100%;
43 + height: 100%;
44 + }
45 + .sg-ring-dual circle {
46 + fill: none;
47 + stroke: var(--sg-color);
48 + stroke-width: 4;
49 + stroke-linecap: round;
50 + transform-origin: 50% 50%;
51 + animation: sg-ring-dual-spin var(--sg-duration) linear infinite;
52 + }
53 + .sg-ring-dual-inner {
54 + opacity: 0.55;
55 + animation-direction: reverse;
56 + animation-duration: calc(var(--sg-duration) * 0.7);
57 + }
58 + @keyframes sg-ring-dual-spin {
59 + 100% { transform: rotate(360deg); }
60 + }
61 +</style>
added snippets/loaders/spinner-dash.html +60 −0
@@ -0,0 +1,60 @@
1 +<!--
2 + ============================================================
3 + SVGarden — https://www.svgarden.dev
4 + Author : Simon-Pierre Boucher
5 + Contact: contact@spboucher.ai
6 + File : snippets/loaders/spinner-dash.html
7 + Desc : Rotating arc loader using animated stroke-dasharray
8 + ============================================================
9 +-->
10 +<!--svgarden
11 +title: Dash spinner
12 +slug: spinner-dash
13 +category: loaders
14 +tags: [loader, dasharray, keyframes, infinite]
15 +difficulty: beginner
16 +techniques: [stroke-dasharray, stroke-dashoffset, "@keyframes", transform-rotate]
17 +how_it_works: >
18 + The outer rotation is a simple linear spin of the whole SVG. The "chasing"
19 + effect comes from a second animation on the circle itself: stroke-dasharray
20 + grows the visible arc from a sliver to most of the circumference and back,
21 + while stroke-dashoffset shifts where that arc starts, so the head appears
22 + to run away from the tail.
23 +customizable:
24 + - { var: "--sg-color", label: "Color", type: color, default: "#7F77DD" }
25 + - { var: "--sg-size", label: "Size", type: range, min: 24, max: 120, default: 48, unit: px }
26 + - { var: "--sg-duration", label: "Speed", type: range, min: 0.5, max: 4, step: 0.1, default: 1.5, unit: s }
27 +created: 2026-08-10
28 +-->
29 +<div class="sg-spinner-dash" style="--sg-color:#7F77DD; --sg-size:48px; --sg-duration:1.5s;">
30 + <svg viewBox="0 0 50 50" aria-hidden="true" focusable="false">
31 + <circle cx="25" cy="25" r="20" />
32 + </svg>
33 +</div>
34 +<style>
35 + .sg-spinner-dash {
36 + width: var(--sg-size);
37 + height: var(--sg-size);
38 + }
39 + .sg-spinner-dash svg {
40 + display: block;
41 + width: 100%;
42 + height: 100%;
43 + animation: sg-spinner-dash-rotate calc(var(--sg-duration) * 1.4) linear infinite;
44 + }
45 + .sg-spinner-dash circle {
46 + fill: none;
47 + stroke: var(--sg-color);
48 + stroke-width: 4.5;
49 + stroke-linecap: round;
50 + animation: sg-spinner-dash-arc var(--sg-duration) ease-in-out infinite;
51 + }
52 + @keyframes sg-spinner-dash-rotate {
53 + 100% { transform: rotate(360deg); }
54 + }
55 + @keyframes sg-spinner-dash-arc {
56 + 0% { stroke-dasharray: 1 150; stroke-dashoffset: 0; }
57 + 50% { stroke-dasharray: 90 150; stroke-dashoffset: -35; }
58 + 100% { stroke-dasharray: 90 150; stroke-dashoffset: -124; }
59 + }
60 +</style>
added snippets/morph/blob-morph.html +60 −0
@@ -0,0 +1,60 @@
1 +<!--
2 + ============================================================
3 + SVGarden — https://www.svgarden.dev
4 + Author : Simon-Pierre Boucher
5 + Contact: contact@spboucher.ai
6 + File : snippets/morph/blob-morph.html
7 + Desc : Organic blob morphing between path shapes with SMIL
8 + ============================================================
9 +-->
10 +<!--svgarden
11 +title: Blob morph
12 +slug: blob-morph
13 +category: morph
14 +tags: [morph, blob, smil, organic]
15 +difficulty: advanced
16 +techniques: [SMIL-path-morph, matched-path-structure, calcMode-spline, keySplines]
17 +how_it_works: >
18 + Path morphing only interpolates when every shape has the exact same command
19 + skeleton — here, three blobs each built from four cubic curves, so the
20 + browser can tween control point to control point. A SMIL <animate> on the d
21 + attribute cycles through the values list and back to the start for a seamless
22 + loop; calcMode="spline" with gentle keySplines gives the organic ease between
23 + shapes. Mismatch a single command and the morph snaps instead of flowing.
24 +customizable:
25 + - { var: "--sg-color", label: "Color", type: color, default: "#7F77DD" }
26 + - { var: "--sg-size", label: "Size", type: range, min: 60, max: 240, default: 120, unit: px }
27 +created: 2026-08-10
28 +-->
29 +<div class="sg-blob-morph" style="--sg-color:#7F77DD; --sg-size:120px;">
30 + <svg viewBox="0 0 100 100" aria-hidden="true" focusable="false">
31 + <path>
32 + <animate
33 + attributeName="d"
34 + dur="8s"
35 + repeatCount="indefinite"
36 + calcMode="spline"
37 + keySplines="0.4 0 0.6 1; 0.4 0 0.6 1; 0.4 0 0.6 1"
38 + values="
39 + M50 12 C 70 12, 88 28, 86 48 C 84 68, 66 88, 46 86 C 26 84, 12 66, 14 46 C 16 26, 30 12, 50 12 Z;
40 + M52 8 C 74 14, 92 34, 84 52 C 76 70, 60 92, 42 84 C 24 76, 8 60, 16 42 C 24 24, 30 2, 52 8 Z;
41 + M46 14 C 64 6, 90 22, 90 44 C 90 66, 72 82, 52 88 C 32 94, 10 72, 12 50 C 14 28, 28 22, 46 14 Z;
42 + M50 12 C 70 12, 88 28, 86 48 C 84 68, 66 88, 46 86 C 26 84, 12 66, 14 46 C 16 26, 30 12, 50 12 Z" />
43 + </path>
44 + </svg>
45 +</div>
46 +<style>
47 + .sg-blob-morph {
48 + width: var(--sg-size);
49 + height: var(--sg-size);
50 + }
51 + .sg-blob-morph svg {
52 + display: block;
53 + width: 100%;
54 + height: 100%;
55 + }
56 + .sg-blob-morph path {
57 + fill: var(--sg-color);
58 + opacity: 0.9;
59 + }
60 +</style>
added snippets/morph/play-pause-morph.html +78 −0
@@ -0,0 +1,78 @@
1 +<!--
2 + ============================================================
3 + SVGarden — https://www.svgarden.dev
4 + Author : Simon-Pierre Boucher
5 + Contact: contact@spboucher.ai
6 + File : snippets/morph/play-pause-morph.html
7 + Desc : Play ⇄ pause icon morph on click
8 + ============================================================
9 +-->
10 +<!--svgarden
11 +title: Play ⇄ pause morph
12 +slug: play-pause-morph
13 +category: morph
14 +tags: [morph, play, pause, button, js]
15 +difficulty: advanced
16 +techniques: [css-d-property, matched-path-structure, transition, aria-pressed]
17 +how_it_works: >
18 + The play triangle is secretly two quadrilaterals sharing an edge, because the
19 + pause icon is two quadrilaterals too — matching point counts is what lets the
20 + shapes tween. The script only flips aria-pressed and writes each path's new
21 + d into style.d; the CSS transition on the d property does the actual morph.
22 + Browsers without CSS d support (older Safari) still swap icons instantly via
23 + the attribute fallback, so the button never breaks.
24 +customizable:
25 + - { var: "--sg-color", label: "Color", type: color, default: "#7F77DD" }
26 + - { var: "--sg-size", label: "Size", type: range, min: 32, max: 140, default: 64, unit: px }
27 +created: 2026-08-10
28 +-->
29 +<button class="sg-play-pause-morph" type="button" aria-pressed="false" aria-label="Play" style="--sg-color:#7F77DD; --sg-size:64px;">
30 + <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
31 + <path d="M8 5 L13 7.1 L13 16.9 L8 19 Z" />
32 + <path d="M13 7.1 L18 9.2 L18 14.8 L13 16.9 Z" />
33 + </svg>
34 +</button>
35 +<script>
36 + document.querySelectorAll('.sg-play-pause-morph:not([data-sg-init])').forEach(function (btn) {
37 + btn.setAttribute('data-sg-init', '');
38 + var shapes = {
39 + play: ['M8 5 L13 7.1 L13 16.9 L8 19 Z', 'M13 7.1 L18 9.2 L18 14.8 L13 16.9 Z'],
40 + pause: ['M7 5 L11 5 L11 19 L7 19 Z', 'M14.5 5 L18.5 5 L18.5 19 L14.5 19 Z']
41 + };
42 + btn.addEventListener('click', function () {
43 + var playing = btn.getAttribute('aria-pressed') !== 'true';
44 + btn.setAttribute('aria-pressed', playing);
45 + btn.setAttribute('aria-label', playing ? 'Pause' : 'Play');
46 + btn.querySelectorAll('path').forEach(function (p, i) {
47 + var d = shapes[playing ? 'pause' : 'play'][i];
48 + p.setAttribute('d', d);
49 + p.style.d = 'path("' + d + '")';
50 + });
51 + });
52 + });
53 +</script>
54 +<style>
55 + .sg-play-pause-morph {
56 + appearance: none;
57 + border: 2px solid var(--sg-color);
58 + border-radius: 50%;
59 + background: transparent;
60 + cursor: pointer;
61 + width: var(--sg-size);
62 + height: var(--sg-size);
63 + display: inline-grid;
64 + place-items: center;
65 + transition: background 0.2s ease;
66 + }
67 + .sg-play-pause-morph:hover {
68 + background: rgba(127, 119, 221, 0.12);
69 + }
70 + .sg-play-pause-morph svg {
71 + width: 62%;
72 + height: 62%;
73 + }
74 + .sg-play-pause-morph path {
75 + fill: var(--sg-color);
76 + transition: d 0.35s ease;
77 + }
78 +</style>
added snippets/stroke-draw/checkmark-pop.html +70 −0
@@ -0,0 +1,70 @@
1 +<!--
2 + ============================================================
3 + SVGarden — https://www.svgarden.dev
4 + Author : Simon-Pierre Boucher
5 + Contact: contact@spboucher.ai
6 + File : snippets/stroke-draw/checkmark-pop.html
7 + Desc : Success checkmark — circle draws, check draws, then pops
8 + ============================================================
9 +-->
10 +<!--svgarden
11 +title: Checkmark pop
12 +slug: checkmark-pop
13 +category: stroke-draw
14 +tags: [draw, success, checkmark, sequence]
15 +difficulty: intermediate
16 +techniques: [pathLength, stroke-dashoffset, animation-delay, cubic-bezier-overshoot]
17 +how_it_works: >
18 + Two draw animations run in sequence: the circle traces itself first, then the
19 + check stroke starts after a delay of half the duration — sequencing with
20 + animation-delay instead of one giant keyframe block keeps each part reusable.
21 + The final pop is a scale keyframe with a cubic-bezier whose y-value overshoots
22 + past 1, which is what gives the bounce without any JavaScript spring physics.
23 +customizable:
24 + - { var: "--sg-color", label: "Color", type: color, default: "#2BA05A" }
25 + - { var: "--sg-size", label: "Size", type: range, min: 32, max: 160, default: 72, unit: px }
26 + - { var: "--sg-duration", label: "Speed", type: range, min: 0.6, max: 3, step: 0.1, default: 1, unit: s }
27 +created: 2026-08-10
28 +-->
29 +<div class="sg-checkmark-pop" style="--sg-color:#2BA05A; --sg-size:72px; --sg-duration:1s;">
30 + <svg viewBox="0 0 52 52" role="img" aria-label="Success">
31 + <title>Success</title>
32 + <circle class="sg-checkmark-pop-ring" cx="26" cy="26" r="23" pathLength="100" />
33 + <path class="sg-checkmark-pop-check" d="M15 27 L23 34 L37 19" pathLength="100" />
34 + </svg>
35 +</div>
36 +<style>
37 + .sg-checkmark-pop {
38 + width: var(--sg-size);
39 + height: var(--sg-size);
40 + }
41 + .sg-checkmark-pop svg {
42 + display: block;
43 + width: 100%;
44 + height: 100%;
45 + transform-origin: 50% 50%;
46 + animation: sg-checkmark-pop-scale calc(var(--sg-duration) * 0.5) cubic-bezier(0.3, 1.8, 0.5, 1) calc(var(--sg-duration) * 1.1) both;
47 + }
48 + .sg-checkmark-pop-ring,
49 + .sg-checkmark-pop-check {
50 + fill: none;
51 + stroke: var(--sg-color);
52 + stroke-width: 4;
53 + stroke-linecap: round;
54 + stroke-linejoin: round;
55 + stroke-dasharray: 100;
56 + stroke-dashoffset: 100;
57 + animation: sg-checkmark-pop-draw var(--sg-duration) ease-out forwards;
58 + }
59 + .sg-checkmark-pop-check {
60 + animation-duration: calc(var(--sg-duration) * 0.6);
61 + animation-delay: calc(var(--sg-duration) * 0.5);
62 + }
63 + @keyframes sg-checkmark-pop-draw {
64 + to { stroke-dashoffset: 0; }
65 + }
66 + @keyframes sg-checkmark-pop-scale {
67 + 0% { transform: scale(0.85); }
68 + 100% { transform: scale(1); }
69 + }
70 +</style>
added snippets/stroke-draw/circuit-trace.html +86 −0
@@ -0,0 +1,86 @@
1 +<!--
2 + ============================================================
3 + SVGarden — https://www.svgarden.dev
4 + Author : Simon-Pierre Boucher
5 + Contact: contact@spboucher.ai
6 + File : snippets/stroke-draw/circuit-trace.html
7 + Desc : Circuit-board traces drawing themselves with staggered delays
8 + ============================================================
9 +-->
10 +<!--svgarden
11 +title: Circuit trace
12 +slug: circuit-trace
13 +category: stroke-draw
14 +tags: [draw, circuit, stagger, polyline]
15 +difficulty: intermediate
16 +techniques: [pathLength, stroke-dashoffset, animation-delay, nth-of-type]
17 +how_it_works: >
18 + Four polylines share one dashoffset draw animation, but nth-of-type hands each
19 + a growing animation-delay, so the traces fan out from the chip one after
20 + another like power flowing through a board. The solder pads are circles whose
21 + opacity keyframe is timed to land exactly when their trace arrives — matching
22 + delays across different elements is the whole trick to multi-part choreography.
23 +customizable:
24 + - { var: "--sg-color", label: "Trace color", type: color, default: "#7F77DD" }
25 + - { var: "--sg-size", label: "Size", type: range, min: 80, max: 320, default: 160, unit: px }
26 + - { var: "--sg-duration", label: "Speed", type: range, min: 1, max: 6, step: 0.1, default: 2.4, unit: s }
27 +created: 2026-08-10
28 +-->
29 +<div class="sg-circuit-trace" style="--sg-color:#7F77DD; --sg-size:160px; --sg-duration:2.4s;">
30 + <svg viewBox="0 0 120 80" aria-hidden="true" focusable="false">
31 + <rect class="sg-circuit-trace-chip" x="48" y="30" width="24" height="20" rx="3" />
32 + <polyline pathLength="100" points="48,36 30,36 30,16 14,16" />
33 + <polyline pathLength="100" points="48,44 24,44 24,64 10,64" />
34 + <polyline pathLength="100" points="72,36 92,36 92,14 108,14" />
35 + <polyline pathLength="100" points="72,44 96,44 96,66 110,66" />
36 + <circle cx="14" cy="16" r="3" />
37 + <circle cx="10" cy="64" r="3" />
38 + <circle cx="108" cy="14" r="3" />
39 + <circle cx="110" cy="66" r="3" />
40 + </svg>
41 +</div>
42 +<style>
43 + .sg-circuit-trace {
44 + width: var(--sg-size);
45 + }
46 + .sg-circuit-trace svg {
47 + display: block;
48 + width: 100%;
49 + }
50 + .sg-circuit-trace-chip {
51 + fill: none;
52 + stroke: var(--sg-color);
53 + stroke-width: 2.5;
54 + }
55 + .sg-circuit-trace polyline {
56 + fill: none;
57 + stroke: var(--sg-color);
58 + stroke-width: 2;
59 + stroke-linecap: round;
60 + stroke-linejoin: round;
61 + stroke-dasharray: 100;
62 + stroke-dashoffset: 100;
63 + animation: sg-circuit-trace-draw var(--sg-duration) ease-out infinite;
64 + }
65 + .sg-circuit-trace polyline:nth-of-type(2) { animation-delay: calc(var(--sg-duration) * 0.12); }
66 + .sg-circuit-trace polyline:nth-of-type(3) { animation-delay: calc(var(--sg-duration) * 0.24); }
67 + .sg-circuit-trace polyline:nth-of-type(4) { animation-delay: calc(var(--sg-duration) * 0.36); }
68 + .sg-circuit-trace circle {
69 + fill: var(--sg-color);
70 + opacity: 0;
71 + animation: sg-circuit-trace-pad var(--sg-duration) linear infinite;
72 + }
73 + .sg-circuit-trace circle:nth-of-type(2) { animation-delay: calc(var(--sg-duration) * 0.12); }
74 + .sg-circuit-trace circle:nth-of-type(3) { animation-delay: calc(var(--sg-duration) * 0.24); }
75 + .sg-circuit-trace circle:nth-of-type(4) { animation-delay: calc(var(--sg-duration) * 0.36); }
76 + @keyframes sg-circuit-trace-draw {
77 + 0% { stroke-dashoffset: 100; }
78 + 45%, 80% { stroke-dashoffset: 0; opacity: 1; }
79 + 100% { stroke-dashoffset: 0; opacity: 0; }
80 + }
81 + @keyframes sg-circuit-trace-pad {
82 + 0%, 40% { opacity: 0; }
83 + 50%, 80% { opacity: 1; }
84 + 100% { opacity: 0; }
85 + }
86 +</style>
added snippets/stroke-draw/signature-draw.html +60 −0
@@ -0,0 +1,60 @@
1 +<!--
2 + ============================================================
3 + SVGarden — https://www.svgarden.dev
4 + Author : Simon-Pierre Boucher
5 + Contact: contact@spboucher.ai
6 + File : snippets/stroke-draw/signature-draw.html
7 + Desc : A scripted path hand-drawing itself with dashoffset
8 + ============================================================
9 +-->
10 +<!--svgarden
11 +title: Signature draw
12 +slug: signature-draw
13 +category: stroke-draw
14 +tags: [draw, signature, dashoffset, pathLength]
15 +difficulty: beginner
16 +techniques: [pathLength, stroke-dasharray, stroke-dashoffset, animation-fill-mode]
17 +how_it_works: >
18 + The classic line-drawing trick: set stroke-dasharray to the path's length so
19 + the entire stroke is one long gap, then animate stroke-dashoffset from that
20 + length down to zero — the dash slides into view and the pen appears to write.
21 + Adding pathLength="100" to the path normalizes its length to 100, so you never
22 + have to measure real path lengths with getTotalLength().
23 +customizable:
24 + - { var: "--sg-color", label: "Ink", type: color, default: "#7F77DD" }
25 + - { var: "--sg-size", label: "Width", type: range, min: 80, max: 360, default: 180, unit: px }
26 + - { var: "--sg-duration", label: "Speed", type: range, min: 0.8, max: 6, step: 0.1, default: 2.5, unit: s }
27 +created: 2026-08-10
28 +-->
29 +<div class="sg-signature-draw" style="--sg-color:#7F77DD; --sg-size:180px; --sg-duration:2.5s;">
30 + <svg viewBox="0 0 160 60" aria-hidden="true" focusable="false">
31 + <path
32 + pathLength="100"
33 + d="M12 42 C 18 20, 30 14, 32 26 C 34 38, 22 50, 30 46 C 40 40, 46 22, 52 28
34 + C 56 32, 52 44, 60 40 C 68 36, 70 24, 78 28 C 84 31, 80 42, 88 39
35 + C 98 35, 102 26, 112 30 C 120 33, 118 42, 128 38 C 136 34, 142 30, 148 32" />
36 + </svg>
37 +</div>
38 +<style>
39 + .sg-signature-draw {
40 + width: var(--sg-size);
41 + }
42 + .sg-signature-draw svg {
43 + display: block;
44 + width: 100%;
45 + }
46 + .sg-signature-draw path {
47 + fill: none;
48 + stroke: var(--sg-color);
49 + stroke-width: 2.5;
50 + stroke-linecap: round;
51 + stroke-dasharray: 100;
52 + stroke-dashoffset: 100;
53 + animation: sg-signature-draw-write var(--sg-duration) ease-in-out infinite;
54 + }
55 + @keyframes sg-signature-draw-write {
56 + 0% { stroke-dashoffset: 100; }
57 + 70%, 85% { stroke-dashoffset: 0; opacity: 1; }
58 + 100% { stroke-dashoffset: 0; opacity: 0; }
59 + }
60 +</style>
added snippets/stroke-draw/underline-sketch.html +66 −0
@@ -0,0 +1,66 @@
1 +<!--
2 + ============================================================
3 + SVGarden — https://www.svgarden.dev
4 + Author : Simon-Pierre Boucher
5 + Contact: contact@spboucher.ai
6 + File : snippets/stroke-draw/underline-sketch.html
7 + Desc : Sketchy hand-drawn underline that draws itself on load
8 + ============================================================
9 +-->
10 +<!--svgarden
11 +title: Sketch underline
12 +slug: underline-sketch
13 +category: stroke-draw
14 +tags: [draw, underline, text-decoration, on-load]
15 +difficulty: beginner
16 +techniques: [pathLength, stroke-dashoffset, inline-svg-in-text, animation-fill-mode]
17 +how_it_works: >
18 + The underline is a wobbly two-pass path (it doubles back on itself like a real
19 + pen stroke) positioned under the text with an absolutely-placed SVG. It draws
20 + once on load using the dasharray/dashoffset reveal, and animation-fill-mode:
21 + forwards keeps the finished stroke on screen instead of snapping back to the
22 + first keyframe when the animation ends.
23 +customizable:
24 + - { var: "--sg-color", label: "Ink", type: color, default: "#7F77DD" }
25 + - { var: "--sg-size", label: "Font size", type: range, min: 14, max: 48, default: 26, unit: px }
26 + - { var: "--sg-duration", label: "Speed", type: range, min: 0.3, max: 3, step: 0.1, default: 0.9, unit: s }
27 +created: 2026-08-10
28 +-->
29 +<span class="sg-underline-sketch" style="--sg-color:#7F77DD; --sg-size:26px; --sg-duration:0.9s;">
30 + emphasis, hand-drawn
31 + <svg viewBox="0 0 200 14" preserveAspectRatio="none" aria-hidden="true" focusable="false">
32 + <path
33 + pathLength="100"
34 + d="M4 8 C 40 4, 80 5, 118 6 C 150 7, 178 6, 196 5 C 170 8, 120 9, 70 10 C 45 10.5, 20 10, 8 9" />
35 + </svg>
36 +</span>
37 +<style>
38 + .sg-underline-sketch {
39 + position: relative;
40 + display: inline-block;
41 + font-size: var(--sg-size);
42 + font-weight: 600;
43 + padding-bottom: 0.3em;
44 + }
45 + .sg-underline-sketch svg {
46 + position: absolute;
47 + left: 0;
48 + bottom: 0;
49 + width: 100%;
50 + height: 0.35em;
51 + overflow: visible;
52 + }
53 + .sg-underline-sketch path {
54 + fill: none;
55 + stroke: var(--sg-color);
56 + stroke-width: 2.5;
57 + stroke-linecap: round;
58 + vector-effect: non-scaling-stroke;
59 + stroke-dasharray: 100;
60 + stroke-dashoffset: 100;
61 + animation: sg-underline-sketch-draw var(--sg-duration) ease-out 0.3s forwards;
62 + }
63 + @keyframes sg-underline-sketch-draw {
64 + to { stroke-dashoffset: 0; }
65 + }
66 +</style>
added snippets/text/text-on-path.html +63 −0
@@ -0,0 +1,63 @@
1 +<!--
2 + ============================================================
3 + SVGarden — https://www.svgarden.dev
4 + Author : Simon-Pierre Boucher
5 + Contact: contact@spboucher.ai
6 + File : snippets/text/text-on-path.html
7 + Desc : Text sliding along a curved path with animated startOffset
8 + ============================================================
9 +-->
10 +<!--svgarden
11 +title: Text on a path
12 +slug: text-on-path
13 +category: text
14 +tags: [text, textPath, smil, curve]
15 +difficulty: intermediate
16 +techniques: [textPath, startOffset, SMIL-animate, defs]
17 +how_it_works: >
18 + A <textPath> glues the glyphs to any path you give it, and startOffset says
19 + how far along the curve the text begins. startOffset is an SVG attribute, not
20 + a CSS property, so CSS keyframes can't touch it — instead a tiny SMIL
21 + <animate> element sweeps it from −60% to 110%, sending the sentence surfing
22 + along the wave forever. SMIL ships inside the SVG itself: still zero
23 + JavaScript, still one self-contained file.
24 +customizable:
25 + - { var: "--sg-color", label: "Text color", type: color, default: "#7F77DD" }
26 + - { var: "--sg-size", label: "Width", type: range, min: 160, max: 480, default: 300, unit: px }
27 +created: 2026-08-10
28 +-->
29 +<div class="sg-text-on-path" style="--sg-color:#7F77DD; --sg-size:300px;">
30 + <svg viewBox="0 0 300 90" aria-hidden="true" focusable="false">
31 + <defs>
32 + <path id="sg-text-on-path-curve" d="M8 55 C 60 15, 110 85, 160 50 C 205 20, 250 70, 292 45" />
33 + </defs>
34 + <use href="#sg-text-on-path-curve" class="sg-text-on-path-rail" />
35 + <text class="sg-text-on-path-text">
36 + <textPath href="#sg-text-on-path-curve" startOffset="-60%">
37 + surfing along the bézier wave…
38 + <animate attributeName="startOffset" from="-60%" to="110%" dur="7s" repeatCount="indefinite" />
39 + </textPath>
40 + </text>
41 + </svg>
42 +</div>
43 +<style>
44 + .sg-text-on-path {
45 + width: var(--sg-size);
46 + }
47 + .sg-text-on-path svg {
48 + display: block;
49 + width: 100%;
50 + }
51 + .sg-text-on-path-rail {
52 + fill: none;
53 + stroke: rgba(128, 128, 128, 0.35);
54 + stroke-width: 1.5;
55 + stroke-dasharray: 4 5;
56 + }
57 + .sg-text-on-path-text {
58 + font-family: inherit;
59 + font-size: 15px;
60 + font-weight: 600;
61 + fill: var(--sg-color);
62 + }
63 +</style>
added snippets/text/text-stroke-reveal.html +63 −0
@@ -0,0 +1,63 @@
1 +<!--
2 + ============================================================
3 + SVGarden — https://www.svgarden.dev
4 + Author : Simon-Pierre Boucher
5 + Contact: contact@spboucher.ai
6 + File : snippets/text/text-stroke-reveal.html
7 + Desc : Outlined text that traces itself, then fills with color
8 + ============================================================
9 +-->
10 +<!--svgarden
11 +title: Stroke-reveal text
12 +slug: text-stroke-reveal
13 +category: text
14 +tags: [text, outline, dashoffset, reveal]
15 +difficulty: intermediate
16 +techniques: [svg-text-stroke, stroke-dasharray, fill-opacity, animation-choreography]
17 +how_it_works: >
18 + stroke-dasharray works on <text> too — each glyph's outline is a path, so the
19 + same dash trick that draws lines can trace letterforms. Because every glyph
20 + has a different outline length, one shared dasharray makes them finish at
21 + slightly different moments, which reads as pleasantly hand-made rather than
22 + mechanical. Once the trace is mostly done, a second phase of the same
23 + keyframe fades fill-opacity in to "ink" the letters.
24 +customizable:
25 + - { var: "--sg-color", label: "Color", type: color, default: "#7F77DD" }
26 + - { var: "--sg-size", label: "Width", type: range, min: 120, max: 420, default: 240, unit: px }
27 + - { var: "--sg-duration", label: "Speed", type: range, min: 1.5, max: 8, step: 0.5, default: 4, unit: s }
28 +created: 2026-08-10
29 +-->
30 +<div class="sg-text-stroke-reveal" style="--sg-color:#7F77DD; --sg-size:240px; --sg-duration:4s;">
31 + <svg viewBox="0 0 240 60" aria-hidden="true" focusable="false">
32 + <text x="120" y="42">SVGarden</text>
33 + </svg>
34 +</div>
35 +<style>
36 + .sg-text-stroke-reveal {
37 + width: var(--sg-size);
38 + }
39 + .sg-text-stroke-reveal svg {
40 + display: block;
41 + width: 100%;
42 + }
43 + .sg-text-stroke-reveal text {
44 + font-family: inherit;
45 + font-size: 44px;
46 + font-weight: 700;
47 + letter-spacing: 1px;
48 + text-anchor: middle;
49 + fill: var(--sg-color);
50 + fill-opacity: 0;
51 + stroke: var(--sg-color);
52 + stroke-width: 1.2;
53 + stroke-dasharray: 220;
54 + stroke-dashoffset: 220;
55 + animation: sg-text-stroke-reveal-ink var(--sg-duration) ease-in-out infinite;
56 + }
57 + @keyframes sg-text-stroke-reveal-ink {
58 + 0% { stroke-dashoffset: 220; fill-opacity: 0; }
59 + 55% { stroke-dashoffset: 0; fill-opacity: 0; }
60 + 70%, 88% { stroke-dashoffset: 0; fill-opacity: 1; }
61 + 100% { stroke-dashoffset: 0; fill-opacity: 0; }
62 + }
63 +</style>
added snippets/text/wave-text.html +69 −0
@@ -0,0 +1,69 @@
1 +<!--
2 + ============================================================
3 + SVGarden — https://www.svgarden.dev
4 + Author : Simon-Pierre Boucher
5 + Contact: contact@spboucher.ai
6 + File : snippets/text/wave-text.html
7 + Desc : Letters bouncing in a wave via staggered delays
8 + ============================================================
9 +-->
10 +<!--svgarden
11 +title: Wave text
12 +slug: wave-text
13 +category: text
14 +tags: [text, wave, stagger, bounce]
15 +difficulty: beginner
16 +techniques: [animation-delay, nth-of-type, translateY, svg-text]
17 +how_it_works: >
18 + Each letter is its own <text> element running the same small translateY
19 + bounce; the traveling wave is nothing but arithmetic — every letter's
20 + animation-delay is one-tenth of the duration later than its neighbor's,
21 + computed with calc() from the same variable that sets the speed. Change the
22 + fraction and the wave tightens or stretches; make it negative and the wave
23 + rolls the other way.
24 +customizable:
25 + - { var: "--sg-color", label: "Color", type: color, default: "#7F77DD" }
26 + - { var: "--sg-size", label: "Width", type: range, min: 120, max: 400, default: 220, unit: px }
27 + - { var: "--sg-duration", label: "Speed", type: range, min: 0.6, max: 4, step: 0.1, default: 1.6, unit: s }
28 +created: 2026-08-10
29 +-->
30 +<div class="sg-wave-text" style="--sg-color:#7F77DD; --sg-size:220px; --sg-duration:1.6s;">
31 + <svg viewBox="0 0 220 60" aria-hidden="true" focusable="false">
32 + <text x="14" y="40">s</text>
33 + <text x="39" y="40">v</text>
34 + <text x="64" y="40">g</text>
35 + <text x="89" y="40">a</text>
36 + <text x="114" y="40">r</text>
37 + <text x="136" y="40">d</text>
38 + <text x="162" y="40">e</text>
39 + <text x="187" y="40">n</text>
40 + </svg>
41 +</div>
42 +<style>
43 + .sg-wave-text {
44 + width: var(--sg-size);
45 + }
46 + .sg-wave-text svg {
47 + display: block;
48 + width: 100%;
49 + }
50 + .sg-wave-text text {
51 + font-family: inherit;
52 + font-size: 34px;
53 + font-weight: 700;
54 + fill: var(--sg-color);
55 + animation: sg-wave-text-bounce var(--sg-duration) ease-in-out infinite;
56 + }
57 + .sg-wave-text text:nth-of-type(2) { animation-delay: calc(var(--sg-duration) * -0.9); }
58 + .sg-wave-text text:nth-of-type(3) { animation-delay: calc(var(--sg-duration) * -0.8); }
59 + .sg-wave-text text:nth-of-type(4) { animation-delay: calc(var(--sg-duration) * -0.7); }
60 + .sg-wave-text text:nth-of-type(5) { animation-delay: calc(var(--sg-duration) * -0.6); }
61 + .sg-wave-text text:nth-of-type(6) { animation-delay: calc(var(--sg-duration) * -0.5); }
62 + .sg-wave-text text:nth-of-type(7) { animation-delay: calc(var(--sg-duration) * -0.4); }
63 + .sg-wave-text text:nth-of-type(8) { animation-delay: calc(var(--sg-duration) * -0.3); }
64 + @keyframes sg-wave-text-bounce {
65 + 0%, 100% { transform: translateY(0); }
66 + 30% { transform: translateY(-9px); }
67 + 60% { transform: translateY(2px); }
68 + }
69 +</style>
70