feat(snippets): add interactive category (6)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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 <details> 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 | ||