SPB Git

spb/svgarden Public

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

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

feat(snippets): add scroll category (6, scroll-driven animations)

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

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 ·&#160;</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