feat(snippets): 24 seed snippets across 7 categories (§7 list)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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 | ||