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 interactive category (6)

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

Showing 6 changed files with +726 and −0

added snippets/interactive/accordion-chevron-morph.html +127 −0
@@ -0,0 +1,127 @@
1 +<!--
2 + ============================================================
3 + SVGarden — https://www.svgarden.dev
4 + Author : Simon-Pierre Boucher
5 + Contact: contact@spboucher.ai
6 + File : snippets/interactive/accordion-chevron-morph.html
7 + Desc : details/summary accordion — chevron flips, content unclips
8 + ============================================================
9 +-->
10 +<!--svgarden
11 +title: Accordion chevron morph
12 +slug: accordion-chevron-morph
13 +category: interactive
14 +tags: [interactive, accordion, details, disclosure]
15 +difficulty: intermediate
16 +techniques: [details-element, "[open]", "clip-path", "@keyframes", ":focus-visible"]
17 +how_it_works: >
18 + The disclosure state lives in the native <details> element — the browser
19 + toggles its [open] attribute, remembers it, and wires up mouse, keyboard and
20 + screen-reader semantics with zero script. CSS watches [open] to flip the
21 + chevron with a scaleY(-1) transform (a transform "morph", no attribute
22 + rewriting needed) and to run a reveal keyframe on the body that animates
23 + clip-path from a fully-clipped inset to none. Clip is used deliberately:
24 + height: auto can't be transitioned, but a freshly-matching [open] selector
25 + restarts an animation every time, so opening always sweeps in — closing
26 + snaps shut natively, which reads as intentional.
27 +customizable:
28 + - { var: "--sg-color", label: "Accent", type: color, default: "#7F77DD" }
29 + - { var: "--sg-size", label: "Width", type: range, min: 220, max: 460, default: 300, unit: px }
30 + - { var: "--sg-duration", label: "Speed", type: range, min: 0.15, max: 1, step: 0.05, default: 0.35, unit: s }
31 +created: 2026-08-10
32 +-->
33 +<div class="sg-accordion-chevron-morph" style="--sg-color:#7F77DD; --sg-size:300px; --sg-duration:0.35s;">
34 + <details class="sg-accordion-chevron-morph-item">
35 + <summary class="sg-accordion-chevron-morph-head">
36 + Why one file per snippet?
37 + <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
38 + <polyline points="7 10, 12 15, 17 10" />
39 + </svg>
40 + </summary>
41 + <div class="sg-accordion-chevron-morph-body">
42 + <p>Adding an animation means adding one file — the gallery, search index and category pages regenerate themselves at build time.</p>
43 + </div>
44 + </details>
45 + <details class="sg-accordion-chevron-morph-item">
46 + <summary class="sg-accordion-chevron-morph-head">
47 + Why &lt;details&gt; instead of JS?
48 + <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
49 + <polyline points="7 10, 12 15, 17 10" />
50 + </svg>
51 + </summary>
52 + <div class="sg-accordion-chevron-morph-body">
53 + <p>The element ships with state, keyboard handling and accessibility built in. CSS only decorates what the browser already does.</p>
54 + </div>
55 + </details>
56 +</div>
57 +<style>
58 + .sg-accordion-chevron-morph {
59 + width: var(--sg-size);
60 + border: 1px solid rgba(128, 128, 128, 0.35);
61 + border-radius: 10px;
62 + overflow: hidden;
63 + font-size: 0.95rem;
64 + }
65 + .sg-accordion-chevron-morph-item + .sg-accordion-chevron-morph-item {
66 + border-top: 1px solid rgba(128, 128, 128, 0.35);
67 + }
68 + .sg-accordion-chevron-morph-head {
69 + display: flex;
70 + align-items: center;
71 + justify-content: space-between;
72 + gap: 0.75rem;
73 + padding: 0.7rem 0.9rem;
74 + font-weight: 600;
75 + cursor: pointer;
76 + list-style: none;
77 + user-select: none;
78 + }
79 + .sg-accordion-chevron-morph-head::-webkit-details-marker { display: none; }
80 + .sg-accordion-chevron-morph-head:hover { color: var(--sg-color); }
81 + .sg-accordion-chevron-morph-head:focus-visible {
82 + outline: 2px solid var(--sg-color);
83 + outline-offset: -2px;
84 + border-radius: 8px;
85 + }
86 + .sg-accordion-chevron-morph-head svg {
87 + width: 1.15em;
88 + height: 1.15em;
89 + flex: none;
90 + }
91 + .sg-accordion-chevron-morph-head polyline {
92 + fill: none;
93 + stroke: var(--sg-color);
94 + stroke-width: 2.4;
95 + stroke-linecap: round;
96 + stroke-linejoin: round;
97 + transform-box: fill-box;
98 + transform-origin: center;
99 + transition: transform var(--sg-duration) ease;
100 + }
101 + .sg-accordion-chevron-morph-item[open] polyline {
102 + transform: scaleY(-1);
103 + }
104 + .sg-accordion-chevron-morph-body {
105 + padding: 0 0.9rem 0.8rem;
106 + }
107 + .sg-accordion-chevron-morph-body p {
108 + margin: 0;
109 + opacity: 0.8;
110 + line-height: 1.5;
111 + }
112 + .sg-accordion-chevron-morph-item[open] .sg-accordion-chevron-morph-body {
113 + animation: sg-accordion-chevron-morph-reveal var(--sg-duration) ease;
114 + }
115 + @keyframes sg-accordion-chevron-morph-reveal {
116 + from {
117 + clip-path: inset(0 0 100% 0);
118 + translate: 0 -6px;
119 + opacity: 0;
120 + }
121 + to {
122 + clip-path: inset(0);
123 + translate: 0 0;
124 + opacity: 1;
125 + }
126 + }
127 +</style>
added snippets/interactive/card-3d-tilt.html +91 −0
@@ -0,0 +1,91 @@
1 +<!--
2 + ============================================================
3 + SVGarden — https://www.svgarden.dev
4 + Author : Simon-Pierre Boucher
5 + Contact: contact@spboucher.ai
6 + File : snippets/interactive/card-3d-tilt.html
7 + Desc : Pseudo-3D card tilt with layered depth and a moving sheen
8 + ============================================================
9 +-->
10 +<!--svgarden
11 +title: 3D tilt card
12 +slug: card-3d-tilt
13 +category: interactive
14 +tags: [interactive, card, 3d, hover, depth]
15 +difficulty: intermediate
16 +techniques: [perspective, preserve-3d, "translateZ()", background-position-sheen, ":focus-visible"]
17 +how_it_works: >
18 + Three ingredients make flat markup read as a physical object: perspective on
19 + the wrapper defines how aggressively depth foreshortens, transform-style:
20 + preserve-3d on the card lets its children keep their own Z positions, and
21 + each layer (badge, title, body) sits at a different translateZ, so when the
22 + card rotates on hover the layers shift against each other in true parallax.
23 + The glare is a wide diagonal gradient whose background-position slides on
24 + hover — backgrounds clip to the border-radius, which avoids overflow: hidden
25 + (overflow would force the card flat and kill preserve-3d, the classic
26 + gotcha). tabindex plus :focus-visible gives keyboard users the same tilt.
27 +customizable:
28 + - { var: "--sg-color", label: "Accent", type: color, default: "#7F77DD" }
29 + - { var: "--sg-size", label: "Width", type: range, min: 180, max: 340, default: 240, unit: px }
30 + - { var: "--sg-duration", label: "Speed", type: range, min: 0.2, max: 1.2, step: 0.05, default: 0.5, unit: s }
31 +created: 2026-08-10
32 +-->
33 +<div class="sg-card-3d-tilt" style="--sg-color:#7F77DD; --sg-size:240px; --sg-duration:0.5s;">
34 + <div class="sg-card-3d-tilt-card" tabindex="0">
35 + <svg class="sg-card-3d-tilt-badge" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
36 + <polygon points="12,2 15,9 22,9.6 17,14.4 18.4,21.6 12,18 5.6,21.6 7,14.4 2,9.6 9,9" />
37 + </svg>
38 + <strong class="sg-card-3d-tilt-title">Depth, not decoration</strong>
39 + <span class="sg-card-3d-tilt-text">Hover or focus: the layers sit at different translateZ heights, so the tilt reads as thickness.</span>
40 + </div>
41 +</div>
42 +<style>
43 + .sg-card-3d-tilt {
44 + width: var(--sg-size);
45 + perspective: 900px;
46 + }
47 + .sg-card-3d-tilt-card {
48 + position: relative;
49 + display: flex;
50 + flex-direction: column;
51 + gap: 0.45rem;
52 + padding: 1.15rem 1.2rem 1.3rem;
53 + border: 1px solid rgba(128, 128, 128, 0.35);
54 + border-radius: 14px;
55 + background:
56 + linear-gradient(120deg, transparent 35%, rgba(255, 255, 255, 0.22) 50%, transparent 65%) no-repeat,
57 + rgba(128, 128, 128, 0.1);
58 + background-size: 250% 100%, auto;
59 + background-position: 110% 0, 0 0;
60 + transform-style: preserve-3d;
61 + transition: transform var(--sg-duration) ease, background-position var(--sg-duration) ease;
62 + cursor: pointer;
63 + }
64 + .sg-card-3d-tilt-card:hover,
65 + .sg-card-3d-tilt-card:focus-visible {
66 + transform: rotateX(11deg) rotateY(-13deg);
67 + background-position: -10% 0, 0 0;
68 + }
69 + .sg-card-3d-tilt-card:focus-visible {
70 + outline: 2px solid var(--sg-color);
71 + outline-offset: 4px;
72 + }
73 + .sg-card-3d-tilt-badge {
74 + width: 34px;
75 + transform: translateZ(46px);
76 + }
77 + .sg-card-3d-tilt-badge polygon {
78 + fill: var(--sg-color);
79 + }
80 + .sg-card-3d-tilt-title {
81 + font-size: 1.02rem;
82 + font-weight: 600;
83 + transform: translateZ(28px);
84 + }
85 + .sg-card-3d-tilt-text {
86 + font-size: 0.84rem;
87 + opacity: 0.75;
88 + line-height: 1.5;
89 + transform: translateZ(14px);
90 + }
91 +</style>
added snippets/interactive/password-eye-blink.html +140 −0
@@ -0,0 +1,140 @@
1 +<!--
2 + ============================================================
3 + SVGarden — https://www.svgarden.dev
4 + Author : Simon-Pierre Boucher
5 + Contact: contact@spboucher.ai
6 + File : snippets/interactive/password-eye-blink.html
7 + Desc : Show/hide password field whose eye icon blinks when toggled
8 + ============================================================
9 +-->
10 +<!--svgarden
11 +title: Password eye blink
12 +slug: password-eye-blink
13 +category: interactive
14 +tags: [interactive, password, toggle, checkbox, form]
15 +difficulty: intermediate
16 +techniques: [":checked", sibling-combinator, scaleY-blink, crossfade, ":focus-visible"]
17 +how_it_works: >
18 + A hidden checkbox placed *before* the field holds the shown/hidden state, so
19 + everything after it is reachable with the general sibling combinator — the
20 + CSS-only pattern for "one control drives many elements". The field stacks
21 + two spans (dots and plain text) and :checked crossfades between them, while
22 + the slash over the eye fades away. The blink is the fun part: the eye group
23 + runs a scaleY squeeze keyframe that only exists under the :checked selector,
24 + and because the selector starts matching at the moment you toggle, the
25 + animation restarts precisely then — CSS animations as event handlers.
26 +customizable:
27 + - { var: "--sg-color", label: "Accent", type: color, default: "#7F77DD" }
28 + - { var: "--sg-size", label: "Width", type: range, min: 180, max: 380, default: 250, unit: px }
29 + - { var: "--sg-duration", label: "Speed", type: range, min: 0.15, max: 0.9, step: 0.05, default: 0.4, unit: s }
30 +created: 2026-08-10
31 +-->
32 +<div class="sg-password-eye-blink" style="--sg-color:#7F77DD; --sg-size:250px; --sg-duration:0.4s;">
33 + <input type="checkbox" id="sg-password-eye-blink-cb" class="sg-password-eye-blink-cb" />
34 + <div class="sg-password-eye-blink-field">
35 + <span class="sg-password-eye-blink-text">
36 + <span class="sg-password-eye-blink-dots" aria-hidden="true">••••••••</span>
37 + <span class="sg-password-eye-blink-plain">hunter2!</span>
38 + </span>
39 + <label for="sg-password-eye-blink-cb" class="sg-password-eye-blink-eye" aria-label="Show password">
40 + <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
41 + <g class="sg-password-eye-blink-ball">
42 + <path d="M2.5 12 C 6.5 5.5, 17.5 5.5, 21.5 12 C 17.5 18.5, 6.5 18.5, 2.5 12 Z" />
43 + <circle cx="12" cy="12" r="3.2" />
44 + </g>
45 + <line class="sg-password-eye-blink-slash" x1="4.5" y1="19.5" x2="19.5" y2="4.5" />
46 + </svg>
47 + </label>
48 + </div>
49 +</div>
50 +<style>
51 + .sg-password-eye-blink {
52 + width: var(--sg-size);
53 + font-family: inherit;
54 + }
55 + .sg-password-eye-blink-cb {
56 + position: absolute;
57 + opacity: 0;
58 + width: 1px;
59 + height: 1px;
60 + }
61 + .sg-password-eye-blink-field {
62 + display: flex;
63 + align-items: center;
64 + gap: 0.6rem;
65 + border: 1px solid rgba(128, 128, 128, 0.45);
66 + border-radius: 10px;
67 + padding: 0.6rem 0.75rem;
68 + }
69 + .sg-password-eye-blink-text {
70 + position: relative;
71 + flex: 1;
72 + font-family: ui-monospace, Menlo, monospace;
73 + font-size: 0.95rem;
74 + letter-spacing: 0.06em;
75 + min-height: 1.3em;
76 + }
77 + .sg-password-eye-blink-dots,
78 + .sg-password-eye-blink-plain {
79 + position: absolute;
80 + left: 0;
81 + top: 50%;
82 + translate: 0 -50%;
83 + transition: opacity var(--sg-duration) ease, translate var(--sg-duration) ease;
84 + }
85 + .sg-password-eye-blink-plain {
86 + opacity: 0;
87 + translate: 0 calc(-50% + 5px);
88 + }
89 + .sg-password-eye-blink-eye {
90 + width: 1.5em;
91 + cursor: pointer;
92 + display: block;
93 + flex: none;
94 + }
95 + .sg-password-eye-blink-eye svg {
96 + display: block;
97 + width: 100%;
98 + overflow: visible;
99 + }
100 + .sg-password-eye-blink-ball {
101 + transform-box: fill-box;
102 + transform-origin: center;
103 + }
104 + .sg-password-eye-blink-ball path {
105 + fill: none;
106 + stroke: rgba(128, 128, 128, 0.8);
107 + stroke-width: 1.8;
108 + }
109 + .sg-password-eye-blink-ball circle {
110 + fill: var(--sg-color);
111 + }
112 + .sg-password-eye-blink-slash {
113 + stroke: rgba(128, 128, 128, 0.8);
114 + stroke-width: 1.8;
115 + stroke-linecap: round;
116 + transition: opacity var(--sg-duration) ease;
117 + }
118 + .sg-password-eye-blink-cb:checked ~ .sg-password-eye-blink-field .sg-password-eye-blink-dots {
119 + opacity: 0;
120 + translate: 0 calc(-50% - 5px);
121 + }
122 + .sg-password-eye-blink-cb:checked ~ .sg-password-eye-blink-field .sg-password-eye-blink-plain {
123 + opacity: 1;
124 + translate: 0 -50%;
125 + }
126 + .sg-password-eye-blink-cb:checked ~ .sg-password-eye-blink-field .sg-password-eye-blink-slash {
127 + opacity: 0;
128 + }
129 + .sg-password-eye-blink-cb:checked ~ .sg-password-eye-blink-field .sg-password-eye-blink-ball {
130 + animation: sg-password-eye-blink-wink calc(var(--sg-duration) * 1.4) ease;
131 + }
132 + .sg-password-eye-blink-cb:focus-visible ~ .sg-password-eye-blink-field {
133 + outline: 2px solid var(--sg-color);
134 + outline-offset: 2px;
135 + }
136 + @keyframes sg-password-eye-blink-wink {
137 + 0%, 100% { transform: scaleY(1); }
138 + 45% { transform: scaleY(0.08); }
139 + }
140 +</style>
added snippets/interactive/rating-stars-fluid.html +148 −0
@@ -0,0 +1,148 @@
1 +<!--
2 + ============================================================
3 + SVGarden — https://www.svgarden.dev
4 + Author : Simon-Pierre Boucher
5 + Contact: contact@spboucher.ai
6 + File : snippets/interactive/rating-stars-fluid.html
7 + Desc : Five-star rating with fluid fill and pop, radio-powered, zero JS
8 + ============================================================
9 +-->
10 +<!--svgarden
11 +title: Fluid star rating
12 +slug: rating-stars-fluid
13 +category: interactive
14 +tags: [interactive, rating, stars, radio, form]
15 +difficulty: advanced
16 +techniques: [radio-group, "row-reverse", sibling-combinator, ":checked", "@keyframes"]
17 +how_it_works: >
18 + The rating is a plain radio group — the browser remembers which star is
19 + chosen, CSS just paints it. The classic trick is DOM order: stars are written
20 + 5→1 and the fieldset is flex row-reverse, so the general sibling combinator
21 + (which only reaches *forward* in the DOM) selects the stars that appear to
22 + the *left* of the checked one, and :checked ~ label fills exactly the right
23 + amount. The freshly picked star gets its own + selector, which newly matches
24 + on selection and therefore restarts a pop keyframe (springy scale plus a
25 + fading dotted ring). Radios stay focusable, so arrow keys change the rating
26 + natively.
27 +customizable:
28 + - { var: "--sg-color", label: "Star color", type: color, default: "#EFBB33" }
29 + - { var: "--sg-size", label: "Star size", type: range, min: 20, max: 64, default: 36, unit: px }
30 + - { var: "--sg-duration", label: "Pop speed", type: range, min: 0.2, max: 1.2, step: 0.05, default: 0.5, unit: s }
31 +created: 2026-08-10
32 +-->
33 +<fieldset class="sg-rating-stars-fluid" style="--sg-color:#EFBB33; --sg-size:36px; --sg-duration:0.5s;">
34 + <legend class="sg-rating-stars-fluid-sr">Rate this from one to five stars</legend>
35 + <input class="sg-rating-stars-fluid-in" type="radio" name="sg-rating-stars-fluid" id="sg-rating-stars-fluid-5" value="5" />
36 + <label class="sg-rating-stars-fluid-star" for="sg-rating-stars-fluid-5" aria-label="5 stars">
37 + <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
38 + <circle class="sg-rating-stars-fluid-pop" cx="12" cy="12" r="10" />
39 + <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" />
40 + </svg>
41 + </label>
42 + <input class="sg-rating-stars-fluid-in" type="radio" name="sg-rating-stars-fluid" id="sg-rating-stars-fluid-4" value="4" />
43 + <label class="sg-rating-stars-fluid-star" for="sg-rating-stars-fluid-4" aria-label="4 stars">
44 + <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
45 + <circle class="sg-rating-stars-fluid-pop" cx="12" cy="12" r="10" />
46 + <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" />
47 + </svg>
48 + </label>
49 + <input class="sg-rating-stars-fluid-in" type="radio" name="sg-rating-stars-fluid" id="sg-rating-stars-fluid-3" value="3" />
50 + <label class="sg-rating-stars-fluid-star" for="sg-rating-stars-fluid-3" aria-label="3 stars">
51 + <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
52 + <circle class="sg-rating-stars-fluid-pop" cx="12" cy="12" r="10" />
53 + <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" />
54 + </svg>
55 + </label>
56 + <input class="sg-rating-stars-fluid-in" type="radio" name="sg-rating-stars-fluid" id="sg-rating-stars-fluid-2" value="2" />
57 + <label class="sg-rating-stars-fluid-star" for="sg-rating-stars-fluid-2" aria-label="2 stars">
58 + <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
59 + <circle class="sg-rating-stars-fluid-pop" cx="12" cy="12" r="10" />
60 + <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" />
61 + </svg>
62 + </label>
63 + <input class="sg-rating-stars-fluid-in" type="radio" name="sg-rating-stars-fluid" id="sg-rating-stars-fluid-1" value="1" />
64 + <label class="sg-rating-stars-fluid-star" for="sg-rating-stars-fluid-1" aria-label="1 star">
65 + <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
66 + <circle class="sg-rating-stars-fluid-pop" cx="12" cy="12" r="10" />
67 + <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" />
68 + </svg>
69 + </label>
70 +</fieldset>
71 +<style>
72 + .sg-rating-stars-fluid {
73 + display: flex;
74 + flex-direction: row-reverse;
75 + justify-content: center;
76 + gap: 0.3rem;
77 + border: 0;
78 + margin: 0;
79 + padding: 0;
80 + }
81 + .sg-rating-stars-fluid-sr {
82 + position: absolute;
83 + width: 1px;
84 + height: 1px;
85 + overflow: hidden;
86 + clip-path: inset(50%);
87 + white-space: nowrap;
88 + }
89 + .sg-rating-stars-fluid-in {
90 + position: absolute;
91 + opacity: 0;
92 + width: 1px;
93 + height: 1px;
94 + }
95 + .sg-rating-stars-fluid-star {
96 + width: var(--sg-size);
97 + cursor: pointer;
98 + }
99 + .sg-rating-stars-fluid-star svg {
100 + display: block;
101 + width: 100%;
102 + overflow: visible;
103 + }
104 + .sg-rating-stars-fluid-star polygon {
105 + fill: transparent;
106 + stroke: rgba(128, 128, 128, 0.55);
107 + stroke-width: 1.4;
108 + stroke-linejoin: round;
109 + transform-box: fill-box;
110 + transform-origin: center;
111 + transition: fill 0.15s ease, stroke 0.15s ease;
112 + }
113 + .sg-rating-stars-fluid-star:hover polygon,
114 + .sg-rating-stars-fluid-star:hover ~ .sg-rating-stars-fluid-star polygon,
115 + .sg-rating-stars-fluid-in:checked + .sg-rating-stars-fluid-star polygon,
116 + .sg-rating-stars-fluid-in:checked ~ .sg-rating-stars-fluid-star polygon {
117 + fill: var(--sg-color);
118 + stroke: var(--sg-color);
119 + }
120 + .sg-rating-stars-fluid-in:checked + .sg-rating-stars-fluid-star polygon {
121 + animation: sg-rating-stars-fluid-spring var(--sg-duration) cubic-bezier(0.3, 1.8, 0.5, 1);
122 + }
123 + .sg-rating-stars-fluid-pop {
124 + fill: none;
125 + stroke: var(--sg-color);
126 + stroke-width: 1.2;
127 + stroke-dasharray: 1.5 5;
128 + opacity: 0;
129 + transform-box: fill-box;
130 + transform-origin: center;
131 + }
132 + .sg-rating-stars-fluid-in:checked + .sg-rating-stars-fluid-star .sg-rating-stars-fluid-pop {
133 + animation: sg-rating-stars-fluid-burst var(--sg-duration) ease-out;
134 + }
135 + .sg-rating-stars-fluid-in:focus-visible + .sg-rating-stars-fluid-star {
136 + outline: 2px solid var(--sg-color);
137 + outline-offset: 2px;
138 + border-radius: 6px;
139 + }
140 + @keyframes sg-rating-stars-fluid-spring {
141 + 0% { transform: scale(0.6); }
142 + 100% { transform: scale(1); }
143 + }
144 + @keyframes sg-rating-stars-fluid-burst {
145 + 0% { opacity: 0.9; transform: scale(0.5); }
146 + 100% { opacity: 0; transform: scale(1.5); }
147 + }
148 +</style>
added snippets/interactive/slider-emoji-mood.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/interactive/slider-emoji-mood.html
7 + Desc : Range slider morphing a face from sad to happy via one custom property
8 + ============================================================
9 +-->
10 +<!--svgarden
11 +title: Emoji mood slider
12 +slug: slider-emoji-mood
13 +category: interactive
14 +tags: [interactive, slider, emoji, custom-properties, js]
15 +difficulty: advanced
16 +techniques: ["calc()", "clamp()", custom-property-data-flow, scaleY-mouth-flip, "hsl()"]
17 +how_it_works: >
18 + The script is a courier, not an animator: on input it copies the slider's
19 + value into one --sg-mood custom property on the root, and every visual
20 + response is derived from that number in CSS. The face tint is
21 + hsl(calc(mood × 1.2) …), sweeping the classic mood scale from cross red
22 + through neutral yellow to content green; the
23 + brows tilt through calc() wrapped in clamp() so they can't over-rotate. The
24 + mouth is the clever bit — one smile path scaled by
25 + scaleY(calc((mood − 50) / 50)), so 100 is a full smile, 0 is the same curve
26 + flipped into a frown, and 50 collapses it into a perfectly flat line —
27 + vector-effect="non-scaling-stroke" keeps that line visible, since transforms
28 + would otherwise squash the stroke to nothing along with the geometry.
29 + Transitions on transform and fill smooth every step.
30 +customizable:
31 + - { var: "--sg-size", label: "Size", type: range, min: 90, max: 220, default: 140, unit: px }
32 + - { var: "--sg-duration", label: "Smoothness", type: range, min: 0.05, max: 0.8, step: 0.05, default: 0.25, unit: s }
33 +created: 2026-08-10
34 +-->
35 +<div class="sg-slider-emoji-mood" style="--sg-size:140px; --sg-duration:0.25s; --sg-mood:50;">
36 + <svg viewBox="0 0 64 64" role="img" aria-label="Mood face">
37 + <title>Mood face</title>
38 + <circle class="sg-slider-emoji-mood-face" cx="32" cy="32" r="26" />
39 + <circle class="sg-slider-emoji-mood-pupil" cx="23" cy="27" r="2.8" />
40 + <circle class="sg-slider-emoji-mood-pupil" cx="41" cy="27" r="2.8" />
41 + <line class="sg-slider-emoji-mood-brow" x1="19" y1="19.5" x2="27" y2="19.5" />
42 + <line class="sg-slider-emoji-mood-brow" x1="37" y1="19.5" x2="45" y2="19.5" />
43 + <g class="sg-slider-emoji-mood-mouth">
44 + <path d="M22 41 Q 32 51 42 41" vector-effect="non-scaling-stroke" />
45 + </g>
46 + </svg>
47 + <input class="sg-slider-emoji-mood-slider" type="range" min="0" max="100" value="50" aria-label="Mood, 0 sad to 100 happy" />
48 +</div>
49 +<script>
50 + document.querySelectorAll('.sg-slider-emoji-mood:not([data-sg-init])').forEach(function (root) {
51 + root.setAttribute('data-sg-init', '');
52 + var slider = root.querySelector('.sg-slider-emoji-mood-slider');
53 + slider.addEventListener('input', function () {
54 + root.style.setProperty('--sg-mood', slider.value);
55 + });
56 + });
57 +</script>
58 +<style>
59 + .sg-slider-emoji-mood {
60 + width: var(--sg-size);
61 + display: flex;
62 + flex-direction: column;
63 + align-items: center;
64 + gap: 0.7rem;
65 + }
66 + .sg-slider-emoji-mood svg {
67 + display: block;
68 + width: 100%;
69 + }
70 + .sg-slider-emoji-mood-face {
71 + fill: hsl(calc(var(--sg-mood) * 1.2) 72% 62%);
72 + transition: fill var(--sg-duration) ease;
73 + }
74 + .sg-slider-emoji-mood-pupil {
75 + fill: rgba(25, 25, 35, 0.85);
76 + }
77 + .sg-slider-emoji-mood-brow {
78 + stroke: rgba(25, 25, 35, 0.85);
79 + stroke-width: 2.2;
80 + stroke-linecap: round;
81 + transform-box: fill-box;
82 + transform-origin: center;
83 + transition: transform var(--sg-duration) ease;
84 + transform: rotate(clamp(-14deg, calc((50 - var(--sg-mood)) * 0.28deg), 14deg));
85 + }
86 + .sg-slider-emoji-mood-brow:nth-of-type(2) {
87 + transform: rotate(clamp(-14deg, calc((var(--sg-mood) - 50) * 0.28deg), 14deg));
88 + }
89 + .sg-slider-emoji-mood-mouth {
90 + transform-box: fill-box;
91 + transform-origin: center;
92 + transition: transform var(--sg-duration) ease;
93 + /* +0.04 epsilon: a true scaleY(0) is a singular matrix and vanishes */
94 + transform: scaleY(calc((var(--sg-mood) - 50) / 50 + 0.04));
95 + }
96 + .sg-slider-emoji-mood-mouth path {
97 + fill: none;
98 + stroke: rgba(25, 25, 35, 0.85);
99 + stroke-width: 2.6;
100 + stroke-linecap: round;
101 + }
102 + .sg-slider-emoji-mood-slider {
103 + width: 90%;
104 + accent-color: hsl(calc(var(--sg-mood) * 1.2) 65% 48%);
105 + }
106 +</style>
added snippets/interactive/toggle-day-night.html +114 −0
@@ -0,0 +1,114 @@
1 +<!--
2 + ============================================================
3 + SVGarden — https://www.svgarden.dev
4 + Author : Simon-Pierre Boucher
5 + Contact: contact@spboucher.ai
6 + File : snippets/interactive/toggle-day-night.html
7 + Desc : Sun ⇄ moon scene toggle driven by a hidden checkbox — zero JS
8 + ============================================================
9 +-->
10 +<!--svgarden
11 +title: Day / night toggle
12 +slug: toggle-day-night
13 +category: interactive
14 +tags: [interactive, toggle, checkbox, scene, dark-mode]
15 +difficulty: advanced
16 +techniques: [":checked", sibling-combinator, svg-scene, transition-delay, ":focus-visible"]
17 +how_it_works: >
18 + The single source of truth is a native <input type="checkbox"> — no script,
19 + no data attributes. The label wraps the whole SVG scene, so clicking anywhere
20 + on the pill (or pressing Space while the hidden checkbox is focused) flips
21 + the state, and CSS observes it with the :checked pseudo-class plus the
22 + sibling combinator to sink the sun below the horizon, raise the moon, and
23 + crossfade the sky rect's fill. The stars share one transition but get a
24 + staggered transition-delay, so they only twinkle in after the sky has
25 + darkened. Because state lives in a real form control, keyboard support and
26 + the :focus-visible ring come for free.
27 +customizable:
28 + - { var: "--sg-day", label: "Day sky", type: color, default: "#8ECDF2" }
29 + - { var: "--sg-night", label: "Night sky", type: color, default: "#2B3060" }
30 + - { var: "--sg-size", label: "Size", type: range, min: 90, max: 260, default: 150, unit: px }
31 + - { var: "--sg-duration", label: "Speed", type: range, min: 0.2, max: 2, step: 0.1, default: 0.6, unit: s }
32 +created: 2026-08-10
33 +-->
34 +<div class="sg-toggle-day-night" style="--sg-day:#8ECDF2; --sg-night:#2B3060; --sg-size:150px; --sg-duration:0.6s;">
35 + <input type="checkbox" id="sg-toggle-day-night-cb" class="sg-toggle-day-night-cb" />
36 + <label for="sg-toggle-day-night-cb" class="sg-toggle-day-night-pill" aria-label="Switch between day and night">
37 + <svg viewBox="0 0 120 60" aria-hidden="true" focusable="false">
38 + <rect class="sg-toggle-day-night-sky" x="0" y="0" width="120" height="60" rx="30" />
39 + <g class="sg-toggle-day-night-stars">
40 + <circle cx="34" cy="18" r="1.6" />
41 + <circle cx="52" cy="36" r="1.1" />
42 + <circle cx="68" cy="14" r="1.4" />
43 + <circle cx="84" cy="30" r="1.2" />
44 + </g>
45 + <g class="sg-toggle-day-night-sun">
46 + <circle cx="30" cy="30" r="13" />
47 + <line x1="30" y1="9" x2="30" y2="13" />
48 + <line x1="30" y1="47" x2="30" y2="51" />
49 + <line x1="9" y1="30" x2="13" y2="30" />
50 + <line x1="47" y1="30" x2="51" y2="30" />
51 + </g>
52 + <g class="sg-toggle-day-night-moon">
53 + <circle cx="90" cy="30" r="12" />
54 + <circle class="sg-toggle-day-night-crater" cx="86" cy="26" r="2.6" />
55 + <circle class="sg-toggle-day-night-crater" cx="94" cy="33" r="1.8" />
56 + </g>
57 + </svg>
58 + </label>
59 +</div>
60 +<style>
61 + .sg-toggle-day-night {
62 + width: var(--sg-size);
63 + display: inline-block;
64 + }
65 + .sg-toggle-day-night-cb {
66 + position: absolute;
67 + opacity: 0;
68 + width: 1px;
69 + height: 1px;
70 + }
71 + .sg-toggle-day-night-pill {
72 + display: block;
73 + cursor: pointer;
74 + border-radius: 999px;
75 + }
76 + .sg-toggle-day-night-pill svg {
77 + display: block;
78 + width: 100%;
79 + border-radius: 999px;
80 + }
81 + .sg-toggle-day-night-sky {
82 + fill: var(--sg-day);
83 + transition: fill var(--sg-duration) ease;
84 + }
85 + .sg-toggle-day-night-sun,
86 + .sg-toggle-day-night-moon {
87 + transition: transform var(--sg-duration) cubic-bezier(0.4, 0, 0.2, 1.2);
88 + }
89 + .sg-toggle-day-night-sun circle { fill: #f6c945; }
90 + .sg-toggle-day-night-sun line {
91 + stroke: #f6c945;
92 + stroke-width: 2.5;
93 + stroke-linecap: round;
94 + }
95 + .sg-toggle-day-night-moon { transform: translateY(42px); }
96 + .sg-toggle-day-night-moon circle { fill: #e8ecf6; }
97 + .sg-toggle-day-night-moon .sg-toggle-day-night-crater { fill: rgba(90, 100, 140, 0.35); }
98 + .sg-toggle-day-night-stars circle {
99 + fill: #ffffff;
100 + opacity: 0;
101 + transition: opacity calc(var(--sg-duration) * 0.6) ease;
102 + }
103 + .sg-toggle-day-night-cb:checked ~ .sg-toggle-day-night-pill .sg-toggle-day-night-sky { fill: var(--sg-night); }
104 + .sg-toggle-day-night-cb:checked ~ .sg-toggle-day-night-pill .sg-toggle-day-night-sun { transform: translateY(42px); }
105 + .sg-toggle-day-night-cb:checked ~ .sg-toggle-day-night-pill .sg-toggle-day-night-moon { transform: translateY(0); }
106 + .sg-toggle-day-night-cb:checked ~ .sg-toggle-day-night-pill .sg-toggle-day-night-stars circle { opacity: 1; }
107 + .sg-toggle-day-night-cb:checked ~ .sg-toggle-day-night-pill .sg-toggle-day-night-stars circle:nth-of-type(2) { transition-delay: calc(var(--sg-duration) * 0.3); }
108 + .sg-toggle-day-night-cb:checked ~ .sg-toggle-day-night-pill .sg-toggle-day-night-stars circle:nth-of-type(3) { transition-delay: calc(var(--sg-duration) * 0.5); }
109 + .sg-toggle-day-night-cb:checked ~ .sg-toggle-day-night-pill .sg-toggle-day-night-stars circle:nth-of-type(4) { transition-delay: calc(var(--sg-duration) * 0.7); }
110 + .sg-toggle-day-night-cb:focus-visible ~ .sg-toggle-day-night-pill {
111 + outline: 2px solid var(--sg-day);
112 + outline-offset: 3px;
113 + }
114 +</style>
115