feat(snippets): add scroll category (6, scroll-driven animations)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Showing 6 changed files with +662 and −0
added
snippets/scroll/scroll-counter-gauge.html
+116 −0
@@ -0,0 +1,116 @@ | ||
| 1 | +<!-- | |
| 2 | + ============================================================ | |
| 3 | + SVGarden — https://www.svgarden.dev | |
| 4 | + Author : Simon-Pierre Boucher | |
| 5 | + Contact: contact@spboucher.ai | |
| 6 | + File : snippets/scroll/scroll-counter-gauge.html | |
| 7 | + Desc : Circular gauge that fills with scroll position | |
| 8 | + ============================================================ | |
| 9 | +--> | |
| 10 | +<!--svgarden | |
| 11 | +title: Scroll gauge | |
| 12 | +slug: scroll-counter-gauge | |
| 13 | +category: scroll | |
| 14 | +tags: [scroll, gauge, progress, scroll-timeline] | |
| 15 | +difficulty: advanced | |
| 16 | +techniques: ["animation-timeline: scroll()", pathLength, scale-property, "@supports"] | |
| 17 | +how_it_works: > | |
| 18 | + Two animations ride the same scroll timeline: the arc's stroke-dashoffset | |
| 19 | + runs 100→0 (pathLength="100" makes the circumference exactly 100, so offset | |
| 20 | + equals "percent unscrolled"), while the independent scale property breathes | |
| 21 | + the dial from 0.85 to 1 — using scale instead of transform means it can't | |
| 22 | + collide with the −90° rotation that starts the arc at twelve o'clock. | |
| 23 | + Because both declare animation-duration: auto and scroll(nearest), the | |
| 24 | + box's own scrollbar is the only clock. Unsupported browsers skip the | |
| 25 | + @supports block and simply show the gauge full. | |
| 26 | +support: "Scroll-driven animations need Chrome/Edge 115+; other browsers show the gauge fully filled, static." | |
| 27 | +customizable: | |
| 28 | + - { var: "--sg-color", label: "Color", type: color, default: "#7F77DD" } | |
| 29 | + - { var: "--sg-size", label: "Box height", type: range, min: 180, max: 400, default: 260, unit: px } | |
| 30 | + - { var: "--sg-thick", label: "Thickness", type: range, min: 4, max: 14, default: 8, unit: px } | |
| 31 | +created: 2026-08-10 | |
| 32 | +--> | |
| 33 | +<div class="sg-scroll-counter-gauge" style="--sg-color:#7F77DD; --sg-size:260px; --sg-thick:8px;"> | |
| 34 | + <div class="sg-scroll-counter-gauge-track"> | |
| 35 | + <div class="sg-scroll-counter-gauge-sticky"> | |
| 36 | + <svg viewBox="0 0 100 100" aria-hidden="true" focusable="false"> | |
| 37 | + <circle class="sg-scroll-counter-gauge-rail" cx="50" cy="50" r="42" pathLength="100" /> | |
| 38 | + <circle class="sg-scroll-counter-gauge-arc" cx="50" cy="50" r="42" pathLength="100" /> | |
| 39 | + <text class="sg-scroll-counter-gauge-label" x="50" y="55">scroll</text> | |
| 40 | + </svg> | |
| 41 | + <span class="sg-scroll-counter-gauge-hint">scroll ↓</span> | |
| 42 | + </div> | |
| 43 | + </div> | |
| 44 | +</div> | |
| 45 | +<style> | |
| 46 | + .sg-scroll-counter-gauge { | |
| 47 | + width: 100%; | |
| 48 | + min-width: 200px; | |
| 49 | + height: var(--sg-size); | |
| 50 | + overflow-y: scroll; | |
| 51 | + border: 1px solid rgba(128, 128, 128, 0.3); | |
| 52 | + border-radius: 10px; | |
| 53 | + } | |
| 54 | + .sg-scroll-counter-gauge-track { | |
| 55 | + height: calc(var(--sg-size) * 3); | |
| 56 | + } | |
| 57 | + .sg-scroll-counter-gauge-sticky { | |
| 58 | + position: sticky; | |
| 59 | + top: 0; | |
| 60 | + height: var(--sg-size); | |
| 61 | + display: grid; | |
| 62 | + place-items: center; | |
| 63 | + } | |
| 64 | + .sg-scroll-counter-gauge-sticky svg { | |
| 65 | + width: min(55%, calc(var(--sg-size) * 0.65)); | |
| 66 | + display: block; | |
| 67 | + } | |
| 68 | + .sg-scroll-counter-gauge-hint { | |
| 69 | + position: absolute; | |
| 70 | + right: 12px; | |
| 71 | + bottom: 8px; | |
| 72 | + font-size: 12px; | |
| 73 | + opacity: 0.6; | |
| 74 | + } | |
| 75 | + .sg-scroll-counter-gauge-rail, | |
| 76 | + .sg-scroll-counter-gauge-arc { | |
| 77 | + fill: none; | |
| 78 | + stroke: var(--sg-color); | |
| 79 | + stroke-width: var(--sg-thick); | |
| 80 | + stroke-linecap: round; | |
| 81 | + transform: rotate(-90deg); | |
| 82 | + transform-origin: 50% 50%; | |
| 83 | + } | |
| 84 | + .sg-scroll-counter-gauge-rail { opacity: 0.15; } | |
| 85 | + .sg-scroll-counter-gauge-arc { | |
| 86 | + stroke-dasharray: 100; | |
| 87 | + stroke-dashoffset: 0; /* fallback: full */ | |
| 88 | + } | |
| 89 | + .sg-scroll-counter-gauge-label { | |
| 90 | + font-family: inherit; | |
| 91 | + font-size: 13px; | |
| 92 | + font-weight: 600; | |
| 93 | + fill: var(--sg-color); | |
| 94 | + text-anchor: middle; | |
| 95 | + } | |
| 96 | + @supports (animation-timeline: scroll()) { | |
| 97 | + .sg-scroll-counter-gauge-arc { | |
| 98 | + animation: sg-scroll-counter-gauge-fill linear both; | |
| 99 | + animation-duration: auto; | |
| 100 | + animation-timeline: scroll(nearest); | |
| 101 | + } | |
| 102 | + .sg-scroll-counter-gauge-sticky svg { | |
| 103 | + animation: sg-scroll-counter-gauge-breathe linear both; | |
| 104 | + animation-duration: auto; | |
| 105 | + animation-timeline: scroll(nearest); | |
| 106 | + } | |
| 107 | + } | |
| 108 | + @keyframes sg-scroll-counter-gauge-fill { | |
| 109 | + from { stroke-dashoffset: 100; } | |
| 110 | + to { stroke-dashoffset: 0; } | |
| 111 | + } | |
| 112 | + @keyframes sg-scroll-counter-gauge-breathe { | |
| 113 | + from { scale: 0.85; } | |
| 114 | + to { scale: 1; } | |
| 115 | + } | |
| 116 | +</style> | |
added
snippets/scroll/scroll-parallax-layers.html
+97 −0
@@ -0,0 +1,97 @@ | ||
| 1 | +<!-- | |
| 2 | + ============================================================ | |
| 3 | + SVGarden — https://www.svgarden.dev | |
| 4 | + Author : Simon-Pierre Boucher | |
| 5 | + Contact: contact@spboucher.ai | |
| 6 | + File : snippets/scroll/scroll-parallax-layers.html | |
| 7 | + Desc : Layered SVG mountain scene with scroll-speed parallax depth | |
| 8 | + ============================================================ | |
| 9 | +--> | |
| 10 | +<!--svgarden | |
| 11 | +title: Scroll parallax layers | |
| 12 | +slug: scroll-parallax-layers | |
| 13 | +category: scroll | |
| 14 | +tags: [scroll, parallax, layers, scene] | |
| 15 | +difficulty: advanced | |
| 16 | +techniques: ["animation-timeline: scroll()", "position: sticky", translate-property, "@supports", calc-depth] | |
| 17 | +how_it_works: > | |
| 18 | + The scene is pinned with position: sticky while its parent track is three | |
| 19 | + times taller, so scrolling the box scrubs a timeline without the scene ever | |
| 20 | + leaving view. All three mountain layers share one translate keyframe, but | |
| 21 | + each sets its own --sg-shift (a calc() fraction of the depth variable), so | |
| 22 | + the near layer rises five times farther than the far one — unequal travel | |
| 23 | + for equal scroll is the entire parallax illusion. Browsers without | |
| 24 | + scroll-driven animations skip the @supports block and show the scene as a | |
| 25 | + static composition. | |
| 26 | +support: "Scroll-driven animations need Chrome/Edge 115+; other browsers show the layered scene without parallax motion." | |
| 27 | +customizable: | |
| 28 | + - { var: "--sg-color", label: "Scene color", type: color, default: "#7F77DD" } | |
| 29 | + - { var: "--sg-size", label: "Box height", type: range, min: 180, max: 400, default: 260, unit: px } | |
| 30 | + - { var: "--sg-depth", label: "Depth", type: range, min: 10, max: 80, default: 40 } | |
| 31 | +created: 2026-08-10 | |
| 32 | +--> | |
| 33 | +<div class="sg-scroll-parallax-layers" style="--sg-color:#7F77DD; --sg-size:260px; --sg-depth:40;"> | |
| 34 | + <div class="sg-scroll-parallax-layers-track"> | |
| 35 | + <div class="sg-scroll-parallax-layers-sticky"> | |
| 36 | + <svg viewBox="0 0 200 120" preserveAspectRatio="xMidYMax slice" aria-hidden="true" focusable="false"> | |
| 37 | + <circle class="sg-scroll-parallax-layers-sun" cx="150" cy="30" r="12" /> | |
| 38 | + <path class="sg-scroll-parallax-layers-l1" | |
| 39 | + d="M0 74 L28 42 L52 70 L84 40 L118 72 L148 46 L176 70 L200 56 V140 H0 Z" /> | |
| 40 | + <path class="sg-scroll-parallax-layers-l2" | |
| 41 | + d="M0 90 C 30 68, 62 98, 102 82 C 142 66, 172 96, 200 84 V140 H0 Z" /> | |
| 42 | + <path class="sg-scroll-parallax-layers-l3" | |
| 43 | + d="M0 104 C 40 88, 82 114, 124 100 C 162 88, 184 108, 200 102 V140 H0 Z" /> | |
| 44 | + </svg> | |
| 45 | + <span class="sg-scroll-parallax-layers-hint">scroll ↓</span> | |
| 46 | + </div> | |
| 47 | + </div> | |
| 48 | +</div> | |
| 49 | +<style> | |
| 50 | + .sg-scroll-parallax-layers { | |
| 51 | + width: 100%; | |
| 52 | + min-width: 220px; | |
| 53 | + height: var(--sg-size); | |
| 54 | + overflow-y: scroll; | |
| 55 | + border: 1px solid rgba(128, 128, 128, 0.3); | |
| 56 | + border-radius: 10px; | |
| 57 | + } | |
| 58 | + .sg-scroll-parallax-layers-track { | |
| 59 | + height: calc(var(--sg-size) * 3); | |
| 60 | + } | |
| 61 | + .sg-scroll-parallax-layers-sticky { | |
| 62 | + position: sticky; | |
| 63 | + top: 0; | |
| 64 | + height: var(--sg-size); | |
| 65 | + /* no overflow:hidden here — it would become the nearest scroll container | |
| 66 | + and scroll(nearest) would bind to a box that never scrolls */ | |
| 67 | + } | |
| 68 | + .sg-scroll-parallax-layers-sticky svg { | |
| 69 | + display: block; | |
| 70 | + width: 100%; | |
| 71 | + height: 100%; | |
| 72 | + } | |
| 73 | + .sg-scroll-parallax-layers-hint { | |
| 74 | + position: absolute; | |
| 75 | + right: 12px; | |
| 76 | + top: 8px; | |
| 77 | + font-size: 12px; | |
| 78 | + opacity: 0.6; | |
| 79 | + } | |
| 80 | + .sg-scroll-parallax-layers-sun { fill: var(--sg-color); opacity: 0.4; } | |
| 81 | + .sg-scroll-parallax-layers-l1 { fill: var(--sg-color); opacity: 0.25; --sg-shift: calc(var(--sg-depth) * -0.2px); } | |
| 82 | + .sg-scroll-parallax-layers-l2 { fill: var(--sg-color); opacity: 0.5; --sg-shift: calc(var(--sg-depth) * -0.55px); } | |
| 83 | + .sg-scroll-parallax-layers-l3 { fill: var(--sg-color); opacity: 0.9; --sg-shift: calc(var(--sg-depth) * -1px); } | |
| 84 | + @supports (animation-timeline: scroll()) { | |
| 85 | + .sg-scroll-parallax-layers-l1, | |
| 86 | + .sg-scroll-parallax-layers-l2, | |
| 87 | + .sg-scroll-parallax-layers-l3 { | |
| 88 | + animation: sg-scroll-parallax-layers-rise linear both; | |
| 89 | + animation-duration: auto; | |
| 90 | + animation-timeline: scroll(nearest); | |
| 91 | + } | |
| 92 | + } | |
| 93 | + @keyframes sg-scroll-parallax-layers-rise { | |
| 94 | + from { translate: 0 0; } | |
| 95 | + to { translate: 0 var(--sg-shift); } | |
| 96 | + } | |
| 97 | +</style> | |
added
snippets/scroll/scroll-progress-path.html
+101 −0
@@ -0,0 +1,101 @@ | ||
| 1 | +<!-- | |
| 2 | + ============================================================ | |
| 3 | + SVGarden — https://www.svgarden.dev | |
| 4 | + Author : Simon-Pierre Boucher | |
| 5 | + Contact: contact@spboucher.ai | |
| 6 | + File : snippets/scroll/scroll-progress-path.html | |
| 7 | + Desc : A path draws itself as its container scrolls (scroll-driven animation) | |
| 8 | + ============================================================ | |
| 9 | +--> | |
| 10 | +<!--svgarden | |
| 11 | +title: Scroll-drawn path | |
| 12 | +slug: scroll-progress-path | |
| 13 | +category: scroll | |
| 14 | +tags: [scroll, draw, progress, scroll-timeline] | |
| 15 | +difficulty: advanced | |
| 16 | +techniques: ["animation-timeline: scroll()", "@supports", pathLength, stroke-dashoffset] | |
| 17 | +how_it_works: > | |
| 18 | + animation-timeline: scroll() detaches the keyframes from the clock and binds | |
| 19 | + them to scroll progress — scroll(nearest) means "the nearest scrolling | |
| 20 | + ancestor", which here is the snippet's own overflow-y box, so the demo is | |
| 21 | + fully self-contained. The dashoffset keyframes then map 0%→100% of the | |
| 22 | + scroll range onto the classic line-draw trick, and animation-duration: auto | |
| 23 | + hands timing control entirely to the timeline. Everything sits inside an | |
| 24 | + @supports gate: browsers without scroll-driven animations simply show the | |
| 25 | + path fully drawn instead of broken. | |
| 26 | +support: "Scroll-driven animations need Chrome/Edge 115+ (Firefox behind a flag); unsupported browsers show the path fully drawn, static." | |
| 27 | +customizable: | |
| 28 | + - { var: "--sg-color", label: "Ink", type: color, default: "#7F77DD" } | |
| 29 | + - { var: "--sg-size", label: "Box height", type: range, min: 180, max: 400, default: 260, unit: px } | |
| 30 | + - { var: "--sg-stroke", label: "Stroke", type: range, min: 1, max: 8, step: 0.5, default: 3, unit: px } | |
| 31 | +created: 2026-08-10 | |
| 32 | +--> | |
| 33 | +<div class="sg-scroll-progress-path" style="--sg-color:#7F77DD; --sg-size:260px; --sg-stroke:3px;"> | |
| 34 | + <div class="sg-scroll-progress-path-track"> | |
| 35 | + <div class="sg-scroll-progress-path-sticky"> | |
| 36 | + <svg viewBox="0 0 200 120" aria-hidden="true" focusable="false"> | |
| 37 | + <path class="sg-scroll-progress-path-ghost" pathLength="100" | |
| 38 | + d="M12 90 C 40 20, 60 20, 80 60 C 100 100, 120 100, 140 40 C 150 15, 170 25, 188 45" /> | |
| 39 | + <path class="sg-scroll-progress-path-line" pathLength="100" | |
| 40 | + d="M12 90 C 40 20, 60 20, 80 60 C 100 100, 120 100, 140 40 C 150 15, 170 25, 188 45" /> | |
| 41 | + </svg> | |
| 42 | + <span class="sg-scroll-progress-path-hint">scroll ↓</span> | |
| 43 | + </div> | |
| 44 | + </div> | |
| 45 | +</div> | |
| 46 | +<style> | |
| 47 | + .sg-scroll-progress-path { | |
| 48 | + width: 100%; | |
| 49 | + min-width: 220px; | |
| 50 | + height: var(--sg-size); | |
| 51 | + overflow-y: scroll; | |
| 52 | + border: 1px solid rgba(128, 128, 128, 0.3); | |
| 53 | + border-radius: 10px; | |
| 54 | + } | |
| 55 | + .sg-scroll-progress-path-track { | |
| 56 | + height: calc(var(--sg-size) * 3); | |
| 57 | + } | |
| 58 | + .sg-scroll-progress-path-sticky { | |
| 59 | + position: sticky; | |
| 60 | + top: 0; | |
| 61 | + height: var(--sg-size); | |
| 62 | + display: grid; | |
| 63 | + place-items: center; | |
| 64 | + } | |
| 65 | + .sg-scroll-progress-path-sticky svg { | |
| 66 | + width: 78%; | |
| 67 | + display: block; | |
| 68 | + } | |
| 69 | + .sg-scroll-progress-path-hint { | |
| 70 | + position: absolute; | |
| 71 | + right: 12px; | |
| 72 | + bottom: 8px; | |
| 73 | + font-size: 12px; | |
| 74 | + opacity: 0.6; | |
| 75 | + } | |
| 76 | + .sg-scroll-progress-path-ghost { | |
| 77 | + fill: none; | |
| 78 | + stroke: rgba(128, 128, 128, 0.25); | |
| 79 | + stroke-width: var(--sg-stroke); | |
| 80 | + stroke-linecap: round; | |
| 81 | + } | |
| 82 | + .sg-scroll-progress-path-line { | |
| 83 | + fill: none; | |
| 84 | + stroke: var(--sg-color); | |
| 85 | + stroke-width: var(--sg-stroke); | |
| 86 | + stroke-linecap: round; | |
| 87 | + stroke-dasharray: 100; | |
| 88 | + stroke-dashoffset: 0; /* fallback: fully drawn */ | |
| 89 | + } | |
| 90 | + @supports (animation-timeline: scroll()) { | |
| 91 | + .sg-scroll-progress-path-line { | |
| 92 | + animation: sg-scroll-progress-path-draw linear both; | |
| 93 | + animation-duration: auto; | |
| 94 | + animation-timeline: scroll(nearest); | |
| 95 | + } | |
| 96 | + } | |
| 97 | + @keyframes sg-scroll-progress-path-draw { | |
| 98 | + from { stroke-dashoffset: 100; } | |
| 99 | + to { stroke-dashoffset: 0; } | |
| 100 | + } | |
| 101 | +</style> | |
added
snippets/scroll/scroll-reveal-mask.html
+106 −0
@@ -0,0 +1,106 @@ | ||
| 1 | +<!-- | |
| 2 | + ============================================================ | |
| 3 | + SVGarden — https://www.svgarden.dev | |
| 4 | + Author : Simon-Pierre Boucher | |
| 5 | + Contact: contact@spboucher.ai | |
| 6 | + File : snippets/scroll/scroll-reveal-mask.html | |
| 7 | + Desc : Items unclip themselves as they enter the scroll viewport | |
| 8 | + ============================================================ | |
| 9 | +--> | |
| 10 | +<!--svgarden | |
| 11 | +title: Scroll reveal mask | |
| 12 | +slug: scroll-reveal-mask | |
| 13 | +category: scroll | |
| 14 | +tags: [scroll, reveal, clip-path, view-timeline] | |
| 15 | +difficulty: advanced | |
| 16 | +techniques: ["animation-timeline: view()", animation-range, "clip-path: inset()", "@supports"] | |
| 17 | +how_it_works: > | |
| 18 | + Where scroll() tracks how far a container has scrolled, view() tracks how far | |
| 19 | + a specific element has traveled across the visible viewport of that | |
| 20 | + container — every item below owns its own timeline. animation-range: entry | |
| 21 | + 0% cover 40% confines the clip-path animation to the moment the item enters | |
| 22 | + until it is 40% across, so each row wipes open exactly as you meet it and | |
| 23 | + plays in reverse when you scroll back. The inset() clip is animatable | |
| 24 | + because only its numbers change. Without scroll-driven animation support the | |
| 25 | + @supports block is skipped and the rows are simply visible. | |
| 26 | +support: "view() timelines need Chrome/Edge 115+; other browsers show all rows unmasked." | |
| 27 | +customizable: | |
| 28 | + - { var: "--sg-color", label: "Accent", type: color, default: "#7F77DD" } | |
| 29 | + - { var: "--sg-size", label: "Box height", type: range, min: 180, max: 400, default: 260, unit: px } | |
| 30 | +created: 2026-08-10 | |
| 31 | +--> | |
| 32 | +<div class="sg-scroll-reveal-mask" style="--sg-color:#7F77DD; --sg-size:260px;"> | |
| 33 | + <p class="sg-scroll-reveal-mask-hint">scroll ↓ to reveal</p> | |
| 34 | + <div class="sg-scroll-reveal-mask-item"> | |
| 35 | + <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9" /><path d="M8 12.5 L11 15 L16 9" /></svg> | |
| 36 | + <span class="sg-scroll-reveal-mask-bar" style="width: 72%;"></span> | |
| 37 | + </div> | |
| 38 | + <div class="sg-scroll-reveal-mask-item"> | |
| 39 | + <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9" /><path d="M8 12.5 L11 15 L16 9" /></svg> | |
| 40 | + <span class="sg-scroll-reveal-mask-bar" style="width: 55%;"></span> | |
| 41 | + </div> | |
| 42 | + <div class="sg-scroll-reveal-mask-item"> | |
| 43 | + <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9" /><path d="M8 12.5 L11 15 L16 9" /></svg> | |
| 44 | + <span class="sg-scroll-reveal-mask-bar" style="width: 80%;"></span> | |
| 45 | + </div> | |
| 46 | + <div class="sg-scroll-reveal-mask-item"> | |
| 47 | + <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9" /><path d="M8 12.5 L11 15 L16 9" /></svg> | |
| 48 | + <span class="sg-scroll-reveal-mask-bar" style="width: 63%;"></span> | |
| 49 | + </div> | |
| 50 | +</div> | |
| 51 | +<style> | |
| 52 | + .sg-scroll-reveal-mask { | |
| 53 | + width: 100%; | |
| 54 | + min-width: 220px; | |
| 55 | + height: var(--sg-size); | |
| 56 | + overflow-y: scroll; | |
| 57 | + border: 1px solid rgba(128, 128, 128, 0.3); | |
| 58 | + border-radius: 10px; | |
| 59 | + padding: 0 14px; | |
| 60 | + } | |
| 61 | + .sg-scroll-reveal-mask-hint { | |
| 62 | + margin: 12px 2px calc(var(--sg-size) * 0.55); | |
| 63 | + font-size: 13px; | |
| 64 | + opacity: 0.6; | |
| 65 | + } | |
| 66 | + .sg-scroll-reveal-mask-item { | |
| 67 | + display: flex; | |
| 68 | + align-items: center; | |
| 69 | + gap: 12px; | |
| 70 | + padding: 14px; | |
| 71 | + margin-bottom: calc(var(--sg-size) * 0.5); | |
| 72 | + border: 1px solid rgba(128, 128, 128, 0.3); | |
| 73 | + border-radius: 10px; | |
| 74 | + } | |
| 75 | + .sg-scroll-reveal-mask-item:last-of-type { | |
| 76 | + margin-bottom: calc(var(--sg-size) * 0.35); | |
| 77 | + } | |
| 78 | + .sg-scroll-reveal-mask-item svg { | |
| 79 | + width: 26px; | |
| 80 | + height: 26px; | |
| 81 | + flex: none; | |
| 82 | + fill: none; | |
| 83 | + stroke: var(--sg-color); | |
| 84 | + stroke-width: 2; | |
| 85 | + stroke-linecap: round; | |
| 86 | + stroke-linejoin: round; | |
| 87 | + } | |
| 88 | + .sg-scroll-reveal-mask-bar { | |
| 89 | + height: 8px; | |
| 90 | + border-radius: 4px; | |
| 91 | + background: var(--sg-color); | |
| 92 | + opacity: 0.55; | |
| 93 | + } | |
| 94 | + @supports (animation-timeline: view()) { | |
| 95 | + .sg-scroll-reveal-mask-item { | |
| 96 | + animation: sg-scroll-reveal-mask-wipe linear both; | |
| 97 | + animation-duration: auto; | |
| 98 | + animation-timeline: view(); | |
| 99 | + animation-range: entry 0% cover 40%; | |
| 100 | + } | |
| 101 | + } | |
| 102 | + @keyframes sg-scroll-reveal-mask-wipe { | |
| 103 | + from { clip-path: inset(0 100% 0 0 round 10px); opacity: 0.2; } | |
| 104 | + to { clip-path: inset(0 0 0 0 round 10px); opacity: 1; } | |
| 105 | + } | |
| 106 | +</style> | |
added
snippets/scroll/scroll-rotate-badge.html
+111 −0
@@ -0,0 +1,111 @@ | ||
| 1 | +<!-- | |
| 2 | + ============================================================ | |
| 3 | + SVGarden — https://www.svgarden.dev | |
| 4 | + Author : Simon-Pierre Boucher | |
| 5 | + Contact: contact@spboucher.ai | |
| 6 | + File : snippets/scroll/scroll-rotate-badge.html | |
| 7 | + Desc : Circular text badge that spins with scroll progress | |
| 8 | + ============================================================ | |
| 9 | +--> | |
| 10 | +<!--svgarden | |
| 11 | +title: Scroll-spun badge | |
| 12 | +slug: scroll-rotate-badge | |
| 13 | +category: scroll | |
| 14 | +tags: [scroll, badge, textPath, rotate] | |
| 15 | +difficulty: intermediate | |
| 16 | +techniques: ["animation-timeline: scroll()", textPath, "@supports", progressive-enhancement] | |
| 17 | +how_it_works: > | |
| 18 | + The round label is ordinary textPath typography on a hidden circle; the | |
| 19 | + motion is one rotate keyframe used twice. By default it runs on the regular | |
| 20 | + clock (a slow infinite spin), then an @supports block rebinds the very same | |
| 21 | + keyframes to animation-timeline: scroll(nearest), so the badge's angle | |
| 22 | + becomes a direct readout of how far the box is scrolled — flick fast and it | |
| 23 | + whips around, creep and it crawls. Reusing one @keyframes for both the | |
| 24 | + fallback and the enhancement is the cleanest progressive-enhancement shape | |
| 25 | + for scroll-driven work. | |
| 26 | +support: "Scroll-linked rotation needs Chrome/Edge 115+; other browsers gracefully fall back to a slow time-based spin." | |
| 27 | +customizable: | |
| 28 | + - { var: "--sg-color", label: "Color", type: color, default: "#7F77DD" } | |
| 29 | + - { var: "--sg-size", label: "Box height", type: range, min: 160, max: 360, default: 240, unit: px } | |
| 30 | + - { var: "--sg-duration", label: "Fallback spin", type: range, min: 4, max: 30, default: 14, unit: s } | |
| 31 | +created: 2026-08-10 | |
| 32 | +--> | |
| 33 | +<div class="sg-scroll-rotate-badge" style="--sg-color:#7F77DD; --sg-size:240px; --sg-duration:14s;"> | |
| 34 | + <div class="sg-scroll-rotate-badge-track"> | |
| 35 | + <div class="sg-scroll-rotate-badge-sticky"> | |
| 36 | + <svg viewBox="0 0 100 100" aria-hidden="true" focusable="false"> | |
| 37 | + <defs> | |
| 38 | + <path id="sg-scroll-rotate-badge-o" d="M50 50 m-36 0 a36 36 0 1 1 72 0 a36 36 0 1 1 -72 0" /> | |
| 39 | + </defs> | |
| 40 | + <g class="sg-scroll-rotate-badge-spin"> | |
| 41 | + <text class="sg-scroll-rotate-badge-text"> | |
| 42 | + <textPath href="#sg-scroll-rotate-badge-o">scroll · scroll · scroll · scroll · </textPath> | |
| 43 | + </text> | |
| 44 | + </g> | |
| 45 | + <path class="sg-scroll-rotate-badge-arrow" d="M50 40 V60 M43 54 L50 61 L57 54" /> | |
| 46 | + </svg> | |
| 47 | + <span class="sg-scroll-rotate-badge-hint">scroll ↓</span> | |
| 48 | + </div> | |
| 49 | + </div> | |
| 50 | +</div> | |
| 51 | +<style> | |
| 52 | + .sg-scroll-rotate-badge { | |
| 53 | + width: 100%; | |
| 54 | + min-width: 200px; | |
| 55 | + height: var(--sg-size); | |
| 56 | + overflow-y: scroll; | |
| 57 | + border: 1px solid rgba(128, 128, 128, 0.3); | |
| 58 | + border-radius: 10px; | |
| 59 | + } | |
| 60 | + .sg-scroll-rotate-badge-track { | |
| 61 | + height: calc(var(--sg-size) * 3); | |
| 62 | + } | |
| 63 | + .sg-scroll-rotate-badge-sticky { | |
| 64 | + position: sticky; | |
| 65 | + top: 0; | |
| 66 | + height: var(--sg-size); | |
| 67 | + display: grid; | |
| 68 | + place-items: center; | |
| 69 | + } | |
| 70 | + .sg-scroll-rotate-badge-sticky svg { | |
| 71 | + width: min(60%, calc(var(--sg-size) * 0.7)); | |
| 72 | + display: block; | |
| 73 | + overflow: visible; | |
| 74 | + } | |
| 75 | + .sg-scroll-rotate-badge-hint { | |
| 76 | + position: absolute; | |
| 77 | + right: 12px; | |
| 78 | + bottom: 8px; | |
| 79 | + font-size: 12px; | |
| 80 | + opacity: 0.6; | |
| 81 | + } | |
| 82 | + .sg-scroll-rotate-badge-text { | |
| 83 | + font-family: inherit; | |
| 84 | + font-size: 10.5px; | |
| 85 | + font-weight: 600; | |
| 86 | + letter-spacing: 2.5px; | |
| 87 | + fill: var(--sg-color); | |
| 88 | + } | |
| 89 | + .sg-scroll-rotate-badge-arrow { | |
| 90 | + fill: none; | |
| 91 | + stroke: var(--sg-color); | |
| 92 | + stroke-width: 2.5; | |
| 93 | + stroke-linecap: round; | |
| 94 | + stroke-linejoin: round; | |
| 95 | + } | |
| 96 | + .sg-scroll-rotate-badge-spin { | |
| 97 | + transform-origin: 50% 50%; | |
| 98 | + animation: sg-scroll-rotate-badge-turn var(--sg-duration) linear infinite; | |
| 99 | + } | |
| 100 | + @supports (animation-timeline: scroll()) { | |
| 101 | + .sg-scroll-rotate-badge-spin { | |
| 102 | + animation: sg-scroll-rotate-badge-turn linear both; | |
| 103 | + animation-duration: auto; | |
| 104 | + animation-timeline: scroll(nearest); | |
| 105 | + } | |
| 106 | + } | |
| 107 | + @keyframes sg-scroll-rotate-badge-turn { | |
| 108 | + from { transform: rotate(0turn); } | |
| 109 | + to { transform: rotate(1turn); } | |
| 110 | + } | |
| 111 | +</style> | |
added
snippets/scroll/scroll-scene-day-night.html
+131 −0
@@ -0,0 +1,131 @@ | ||
| 1 | +<!-- | |
| 2 | + ============================================================ | |
| 3 | + SVGarden — https://www.svgarden.dev | |
| 4 | + Author : Simon-Pierre Boucher | |
| 5 | + Contact: contact@spboucher.ai | |
| 6 | + File : snippets/scroll/scroll-scene-day-night.html | |
| 7 | + Desc : Sun sets and moon rises as you scroll the section | |
| 8 | + ============================================================ | |
| 9 | +--> | |
| 10 | +<!--svgarden | |
| 11 | +title: Day → night scroll scene | |
| 12 | +slug: scroll-scene-day-night | |
| 13 | +category: scroll | |
| 14 | +tags: [scroll, scene, day-night, choreography] | |
| 15 | +difficulty: advanced | |
| 16 | +techniques: ["animation-timeline: scroll()", fill-color-keyframes, "color-mix()", translate-property, "@supports"] | |
| 17 | +how_it_works: > | |
| 18 | + Four elements share one scroll(nearest) timeline, each with its own | |
| 19 | + keyframes: the sky rect tweens its SVG fill from the day color to the night | |
| 20 | + color (fill is a CSS property, so it interpolates like any other), the sun | |
| 21 | + translates below the horizon while the moon rises on the mirrored path, and | |
| 22 | + the stars hold at opacity 0 until 55% before fading in. Sharing a timeline | |
| 23 | + is what keeps the choreography locked to scrubbing — reverse scrolling | |
| 24 | + rewinds dawn. The static fallback uses color-mix() to freeze the scene at | |
| 25 | + dusk, halfway between the two customizable sky colors. | |
| 26 | +support: "Scroll-driven animations need Chrome/Edge 115+; other browsers show a static dusk scene (color-mix of the two sky colors)." | |
| 27 | +customizable: | |
| 28 | + - { var: "--sg-sky-day", label: "Day sky", type: color, default: "#8FC7EE" } | |
| 29 | + - { var: "--sg-sky-night", label: "Night sky", type: color, default: "#1B2140" } | |
| 30 | + - { var: "--sg-size", label: "Box height", type: range, min: 180, max: 400, default: 260, unit: px } | |
| 31 | +created: 2026-08-10 | |
| 32 | +--> | |
| 33 | +<div class="sg-scroll-scene-day-night" style="--sg-sky-day:#8FC7EE; --sg-sky-night:#1B2140; --sg-size:260px;"> | |
| 34 | + <div class="sg-scroll-scene-day-night-track"> | |
| 35 | + <div class="sg-scroll-scene-day-night-sticky"> | |
| 36 | + <svg viewBox="0 0 200 120" preserveAspectRatio="xMidYMid slice" aria-hidden="true" focusable="false"> | |
| 37 | + <rect class="sg-scroll-scene-day-night-sky" x="0" y="0" width="200" height="120" /> | |
| 38 | + <g class="sg-scroll-scene-day-night-stars"> | |
| 39 | + <circle cx="30" cy="22" r="1.3" /><circle cx="70" cy="14" r="1" /> | |
| 40 | + <circle cx="120" cy="26" r="1.2" /><circle cx="168" cy="16" r="1" /> | |
| 41 | + </g> | |
| 42 | + <circle class="sg-scroll-scene-day-night-sun" cx="52" cy="42" r="14" /> | |
| 43 | + <g class="sg-scroll-scene-day-night-moon"> | |
| 44 | + <circle cx="146" cy="38" r="11" /> | |
| 45 | + <circle class="sg-scroll-scene-day-night-crater" cx="142" cy="35" r="2.6" /> | |
| 46 | + <circle class="sg-scroll-scene-day-night-crater" cx="150" cy="42" r="1.8" /> | |
| 47 | + </g> | |
| 48 | + <path class="sg-scroll-scene-day-night-hills" d="M0 92 C 40 76, 76 100, 112 88 C 150 76, 176 96, 200 90 V120 H0 Z" /> | |
| 49 | + </svg> | |
| 50 | + <span class="sg-scroll-scene-day-night-hint">scroll ↓</span> | |
| 51 | + </div> | |
| 52 | + </div> | |
| 53 | +</div> | |
| 54 | +<style> | |
| 55 | + .sg-scroll-scene-day-night { | |
| 56 | + width: 100%; | |
| 57 | + min-width: 220px; | |
| 58 | + height: var(--sg-size); | |
| 59 | + overflow-y: scroll; | |
| 60 | + border: 1px solid rgba(128, 128, 128, 0.3); | |
| 61 | + border-radius: 10px; | |
| 62 | + } | |
| 63 | + .sg-scroll-scene-day-night-track { height: calc(var(--sg-size) * 3); } | |
| 64 | + .sg-scroll-scene-day-night-sticky { | |
| 65 | + position: sticky; | |
| 66 | + top: 0; | |
| 67 | + height: var(--sg-size); | |
| 68 | + /* no overflow:hidden here — it would become the nearest scroll container | |
| 69 | + and scroll(nearest) would bind to a box that never scrolls */ | |
| 70 | + } | |
| 71 | + .sg-scroll-scene-day-night-sticky svg { | |
| 72 | + display: block; | |
| 73 | + width: 100%; | |
| 74 | + height: 100%; | |
| 75 | + } | |
| 76 | + .sg-scroll-scene-day-night-hint { | |
| 77 | + position: absolute; | |
| 78 | + right: 12px; | |
| 79 | + bottom: 8px; | |
| 80 | + font-size: 12px; | |
| 81 | + color: #ffffff; | |
| 82 | + background: rgba(0, 0, 0, 0.35); | |
| 83 | + padding: 2px 8px; | |
| 84 | + border-radius: 999px; | |
| 85 | + } | |
| 86 | + /* fallback = frozen dusk, halfway between the two skies */ | |
| 87 | + .sg-scroll-scene-day-night-sky { fill: color-mix(in srgb, var(--sg-sky-day) 45%, var(--sg-sky-night)); } | |
| 88 | + .sg-scroll-scene-day-night-sun { fill: #f6c34a; translate: 0 28px; } | |
| 89 | + .sg-scroll-scene-day-night-moon { fill: #e9edf5; translate: 0 30px; } | |
| 90 | + .sg-scroll-scene-day-night-crater { fill: rgba(0, 0, 0, 0.15); } | |
| 91 | + .sg-scroll-scene-day-night-stars { fill: #ffffff; opacity: 0.45; } | |
| 92 | + .sg-scroll-scene-day-night-hills { fill: rgba(18, 22, 34, 0.85); } | |
| 93 | + @supports (animation-timeline: scroll()) { | |
| 94 | + .sg-scroll-scene-day-night-sky { | |
| 95 | + animation: sg-scroll-scene-day-night-dim linear both; | |
| 96 | + animation-duration: auto; | |
| 97 | + animation-timeline: scroll(nearest); | |
| 98 | + } | |
| 99 | + .sg-scroll-scene-day-night-sun { | |
| 100 | + animation: sg-scroll-scene-day-night-set linear both; | |
| 101 | + animation-duration: auto; | |
| 102 | + animation-timeline: scroll(nearest); | |
| 103 | + } | |
| 104 | + .sg-scroll-scene-day-night-moon { | |
| 105 | + animation: sg-scroll-scene-day-night-rise linear both; | |
| 106 | + animation-duration: auto; | |
| 107 | + animation-timeline: scroll(nearest); | |
| 108 | + } | |
| 109 | + .sg-scroll-scene-day-night-stars { | |
| 110 | + animation: sg-scroll-scene-day-night-twinkle linear both; | |
| 111 | + animation-duration: auto; | |
| 112 | + animation-timeline: scroll(nearest); | |
| 113 | + } | |
| 114 | + } | |
| 115 | + @keyframes sg-scroll-scene-day-night-dim { | |
| 116 | + from { fill: var(--sg-sky-day); } | |
| 117 | + to { fill: var(--sg-sky-night); } | |
| 118 | + } | |
| 119 | + @keyframes sg-scroll-scene-day-night-set { | |
| 120 | + from { translate: 0 0; } | |
| 121 | + to { translate: 0 62px; } | |
| 122 | + } | |
| 123 | + @keyframes sg-scroll-scene-day-night-rise { | |
| 124 | + from { translate: 0 68px; } | |
| 125 | + to { translate: 0 0; } | |
| 126 | + } | |
| 127 | + @keyframes sg-scroll-scene-day-night-twinkle { | |
| 128 | + 0%, 55% { opacity: 0; } | |
| 129 | + 100% { opacity: 1; } | |
| 130 | + } | |
| 131 | +</style> | |
| 132 | ||