feat(snippets): add text-fx category (6)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Showing 6 changed files with +433 and −0
added
snippets/text-fx/text-knockout-video-feel.html
+76 −0
@@ -0,0 +1,76 @@ | ||
| 1 | +<!-- | |
| 2 | + ============================================================ | |
| 3 | + SVGarden — https://www.svgarden.dev | |
| 4 | + Author : Simon-Pierre Boucher | |
| 5 | + Contact: contact@spboucher.ai | |
| 6 | + File : snippets/text-fx/text-knockout-video-feel.html | |
| 7 | + Desc : Animated gradient sheet visible only through knockout letterforms | |
| 8 | + ============================================================ | |
| 9 | +--> | |
| 10 | +<!--svgarden | |
| 11 | +title: Knockout gradient text | |
| 12 | +slug: text-knockout-video-feel | |
| 13 | +category: text-fx | |
| 14 | +tags: [text, mask, knockout, gradient] | |
| 15 | +difficulty: advanced | |
| 16 | +techniques: [svg-mask, mask-knockout, spreadMethod-repeat, translate-property] | |
| 17 | +how_it_works: > | |
| 18 | + An SVG <mask> is a grayscale stencil: white areas of the mask let the masked | |
| 19 | + element show through, black areas erase it. Here the mask is a black rect with | |
| 20 | + white bold <text> on top, so the animated gradient sheet underneath is visible | |
| 21 | + only inside the letterforms — the classic "video-in-text" knockout without any | |
| 22 | + background-clip hacks. The sheet itself is a triple-width rect whose gradient | |
| 23 | + uses spreadMethod="repeat" with a period of exactly 320 units, so sliding it | |
| 24 | + left by one period with the modern translate property loops seamlessly forever. | |
| 25 | +customizable: | |
| 26 | + - { var: "--sg-color", label: "Base color", type: color, default: "#7F77DD" } | |
| 27 | + - { var: "--sg-size", label: "Width", type: range, min: 160, max: 480, default: 320, unit: px } | |
| 28 | + - { var: "--sg-duration", label: "Speed", type: range, min: 2, max: 12, step: 0.5, default: 5, unit: s } | |
| 29 | +created: 2026-08-10 | |
| 30 | +--> | |
| 31 | +<div class="sg-text-knockout-video-feel" style="--sg-color:#7F77DD; --sg-size:320px; --sg-duration:5s;"> | |
| 32 | + <svg viewBox="0 0 320 90" role="img" aria-label="SHINE"> | |
| 33 | + <title>SHINE</title> | |
| 34 | + <defs> | |
| 35 | + <linearGradient id="sg-text-knockout-video-feel-grad" x1="0" y1="0" x2="0.3333" y2="0" spreadMethod="repeat"> | |
| 36 | + <stop offset="0" class="sg-text-knockout-video-feel-stop-a" /> | |
| 37 | + <stop offset="0.33" stop-color="#3fb6f0" /> | |
| 38 | + <stop offset="0.66" stop-color="#e05fa0" /> | |
| 39 | + <stop offset="1" class="sg-text-knockout-video-feel-stop-a" /> | |
| 40 | + </linearGradient> | |
| 41 | + <mask id="sg-text-knockout-video-feel-mask"> | |
| 42 | + <rect x="0" y="0" width="320" height="90" fill="black" /> | |
| 43 | + <text class="sg-text-knockout-video-feel-txt" x="160" y="68" fill="white">SHINE</text> | |
| 44 | + </mask> | |
| 45 | + </defs> | |
| 46 | + <g mask="url(#sg-text-knockout-video-feel-mask)"> | |
| 47 | + <rect class="sg-text-knockout-video-feel-sheet" x="0" y="0" width="960" height="90" | |
| 48 | + fill="url(#sg-text-knockout-video-feel-grad)" /> | |
| 49 | + </g> | |
| 50 | + </svg> | |
| 51 | +</div> | |
| 52 | +<style> | |
| 53 | + .sg-text-knockout-video-feel { | |
| 54 | + width: var(--sg-size); | |
| 55 | + } | |
| 56 | + .sg-text-knockout-video-feel svg { | |
| 57 | + display: block; | |
| 58 | + width: 100%; | |
| 59 | + } | |
| 60 | + .sg-text-knockout-video-feel-txt { | |
| 61 | + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; | |
| 62 | + font-size: 62px; | |
| 63 | + font-weight: 800; | |
| 64 | + letter-spacing: 2px; | |
| 65 | + text-anchor: middle; | |
| 66 | + } | |
| 67 | + .sg-text-knockout-video-feel-stop-a { | |
| 68 | + stop-color: var(--sg-color); | |
| 69 | + } | |
| 70 | + .sg-text-knockout-video-feel-sheet { | |
| 71 | + animation: sg-text-knockout-video-feel-slide var(--sg-duration) linear infinite; | |
| 72 | + } | |
| 73 | + @keyframes sg-text-knockout-video-feel-slide { | |
| 74 | + to { translate: -320px 0; } | |
| 75 | + } | |
| 76 | +</style> | |
added
snippets/text-fx/text-liquid-fill.html
+88 −0
@@ -0,0 +1,88 @@ | ||
| 1 | +<!-- | |
| 2 | + ============================================================ | |
| 3 | + SVGarden — https://www.svgarden.dev | |
| 4 | + Author : Simon-Pierre Boucher | |
| 5 | + Contact: contact@spboucher.ai | |
| 6 | + File : snippets/text-fx/text-liquid-fill.html | |
| 7 | + Desc : Letterforms fill with rising liquid topped by a moving wave | |
| 8 | + ============================================================ | |
| 9 | +--> | |
| 10 | +<!--svgarden | |
| 11 | +title: Liquid-fill text | |
| 12 | +slug: text-liquid-fill | |
| 13 | +category: text-fx | |
| 14 | +tags: [text, clip-path, liquid, wave] | |
| 15 | +difficulty: advanced | |
| 16 | +techniques: [clipPath-from-text, periodic-wave-path, nested-transform-layers, translate-property] | |
| 17 | +how_it_works: > | |
| 18 | + A <clipPath> built from bold <text> confines everything drawn inside it to the | |
| 19 | + letterforms, so the "liquid" only exists where the glyphs are. The liquid is a | |
| 20 | + single path whose top edge is a periodic quadratic wave (q + t commands repeat | |
| 21 | + every 130 units) extending far past the visible area and closing into a deep | |
| 22 | + solid below. Two animations are deliberately split across nested groups so | |
| 23 | + their transforms don't fight: the outer group rises with translateY to raise | |
| 24 | + the water level, while the inner path slides horizontally by exactly one wave | |
| 25 | + period, which makes the surface roll seamlessly. A faint stroked copy of the | |
| 26 | + text on top shows the unfilled portion on any background. | |
| 27 | +customizable: | |
| 28 | + - { var: "--sg-color", label: "Liquid color", type: color, default: "#3F8FDF" } | |
| 29 | + - { var: "--sg-size", label: "Width", type: range, min: 140, max: 420, default: 260, unit: px } | |
| 30 | + - { var: "--sg-duration", label: "Fill time", type: range, min: 2, max: 10, step: 0.5, default: 5, unit: s } | |
| 31 | +created: 2026-08-10 | |
| 32 | +--> | |
| 33 | +<div class="sg-text-liquid-fill" style="--sg-color:#3F8FDF; --sg-size:260px; --sg-duration:5s;"> | |
| 34 | + <svg viewBox="0 0 260 90" role="img" aria-label="FLOW"> | |
| 35 | + <title>FLOW</title> | |
| 36 | + <defs> | |
| 37 | + <clipPath id="sg-text-liquid-fill-clip"> | |
| 38 | + <text class="sg-text-liquid-fill-txt" x="130" y="72">FLOW</text> | |
| 39 | + </clipPath> | |
| 40 | + </defs> | |
| 41 | + <text class="sg-text-liquid-fill-txt sg-text-liquid-fill-ghost" x="130" y="72">FLOW</text> | |
| 42 | + <g clip-path="url(#sg-text-liquid-fill-clip)"> | |
| 43 | + <g class="sg-text-liquid-fill-rise"> | |
| 44 | + <path class="sg-text-liquid-fill-wave" | |
| 45 | + d="M-260 8 q32 -14 65 0 t65 0 t65 0 t65 0 t65 0 t65 0 t65 0 t65 0 V120 H-260 Z" /> | |
| 46 | + </g> | |
| 47 | + </g> | |
| 48 | + </svg> | |
| 49 | +</div> | |
| 50 | +<style> | |
| 51 | + .sg-text-liquid-fill { | |
| 52 | + width: var(--sg-size); | |
| 53 | + } | |
| 54 | + .sg-text-liquid-fill svg { | |
| 55 | + display: block; | |
| 56 | + width: 100%; | |
| 57 | + } | |
| 58 | + .sg-text-liquid-fill-txt { | |
| 59 | + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; | |
| 60 | + font-size: 64px; | |
| 61 | + font-weight: 800; | |
| 62 | + letter-spacing: 2px; | |
| 63 | + text-anchor: middle; | |
| 64 | + } | |
| 65 | + .sg-text-liquid-fill-ghost { | |
| 66 | + fill: none; | |
| 67 | + stroke: var(--sg-color); | |
| 68 | + stroke-width: 1.5; | |
| 69 | + opacity: 0.35; | |
| 70 | + } | |
| 71 | + .sg-text-liquid-fill-rise { | |
| 72 | + animation: sg-text-liquid-fill-up var(--sg-duration) ease-in-out infinite; | |
| 73 | + } | |
| 74 | + .sg-text-liquid-fill-wave { | |
| 75 | + fill: var(--sg-color); | |
| 76 | + animation: sg-text-liquid-fill-roll calc(var(--sg-duration) / 3) linear infinite; | |
| 77 | + } | |
| 78 | + @keyframes sg-text-liquid-fill-up { | |
| 79 | + 0% { transform: translateY(62px); opacity: 1; } | |
| 80 | + 60% { transform: translateY(6px); } | |
| 81 | + 82% { transform: translateY(6px); opacity: 1; } | |
| 82 | + 94% { transform: translateY(6px); opacity: 0; } | |
| 83 | + 100% { transform: translateY(62px); opacity: 0; } | |
| 84 | + } | |
| 85 | + @keyframes sg-text-liquid-fill-roll { | |
| 86 | + to { translate: 130px 0; } | |
| 87 | + } | |
| 88 | +</style> | |
added
snippets/text-fx/text-marquee-path.html
+66 −0
@@ -0,0 +1,66 @@ | ||
| 1 | +<!-- | |
| 2 | + ============================================================ | |
| 3 | + SVGarden — https://www.svgarden.dev | |
| 4 | + Author : Simon-Pierre Boucher | |
| 5 | + Contact: contact@spboucher.ai | |
| 6 | + File : snippets/text-fx/text-marquee-path.html | |
| 7 | + Desc : Infinite marquee flowing along a curved textPath | |
| 8 | + ============================================================ | |
| 9 | +--> | |
| 10 | +<!--svgarden | |
| 11 | +title: Marquee on a path | |
| 12 | +slug: text-marquee-path | |
| 13 | +category: text-fx | |
| 14 | +tags: [text, marquee, textPath, smil] | |
| 15 | +difficulty: intermediate | |
| 16 | +techniques: [textPath, SMIL-startOffset, monospace-periodicity, seamless-loop-math] | |
| 17 | +how_it_works: > | |
| 18 | + The marquee is one long string — a short phrase repeated eight times — glued | |
| 19 | + to a curve with <textPath>, and a SMIL <animate> slides startOffset backward | |
| 20 | + forever. The seamless-loop trick is arithmetic: in a monospace font every | |
| 21 | + repetition has identical width, so shifting by exactly one repetition | |
| 22 | + (here ~25% of the path) lands on a frame indistinguishable from the start. | |
| 23 | + Glyphs that fall before the path's start or past its end are simply not | |
| 24 | + rendered, which is what makes letters appear at one end of the curve and | |
| 25 | + vanish at the other with no clipping mask needed. Tune font-size and the | |
| 26 | + to="-25%" together if you change the phrase. | |
| 27 | +customizable: | |
| 28 | + - { var: "--sg-color", label: "Color", type: color, default: "#7F77DD" } | |
| 29 | + - { var: "--sg-size", label: "Width", type: range, min: 200, max: 520, default: 380, unit: px } | |
| 30 | +created: 2026-08-10 | |
| 31 | +--> | |
| 32 | +<div class="sg-text-marquee-path" style="--sg-color:#7F77DD; --sg-size:380px;"> | |
| 33 | + <svg viewBox="0 0 400 100" aria-hidden="true" focusable="false"> | |
| 34 | + <defs> | |
| 35 | + <path id="sg-text-marquee-path-rail" d="M6 62 C 105 18, 295 98, 394 50" /> | |
| 36 | + </defs> | |
| 37 | + <use href="#sg-text-marquee-path-rail" class="sg-text-marquee-path-guide" /> | |
| 38 | + <text class="sg-text-marquee-path-txt"> | |
| 39 | + <textPath href="#sg-text-marquee-path-rail" startOffset="0%"> | |
| 40 | + svgarden ✦ svgarden ✦ svgarden ✦ svgarden ✦ svgarden ✦ svgarden ✦ svgarden ✦ svgarden ✦ | |
| 41 | + <animate attributeName="startOffset" from="0%" to="-25%" dur="5s" repeatCount="indefinite" /> | |
| 42 | + </textPath> | |
| 43 | + </text> | |
| 44 | + </svg> | |
| 45 | +</div> | |
| 46 | +<style> | |
| 47 | + .sg-text-marquee-path { | |
| 48 | + width: var(--sg-size); | |
| 49 | + } | |
| 50 | + .sg-text-marquee-path svg { | |
| 51 | + display: block; | |
| 52 | + width: 100%; | |
| 53 | + } | |
| 54 | + .sg-text-marquee-path-guide { | |
| 55 | + fill: none; | |
| 56 | + stroke: rgba(128, 128, 128, 0.3); | |
| 57 | + stroke-width: 1; | |
| 58 | + stroke-dasharray: 3 5; | |
| 59 | + } | |
| 60 | + .sg-text-marquee-path-txt { | |
| 61 | + font-family: ui-monospace, 'SF Mono', SFMono-Regular, Menlo, Consolas, monospace; | |
| 62 | + font-size: 15px; | |
| 63 | + font-weight: 600; | |
| 64 | + fill: var(--sg-color); | |
| 65 | + } | |
| 66 | +</style> | |
added
snippets/text-fx/text-scramble-decode.html
+67 −0
@@ -0,0 +1,67 @@ | ||
| 1 | +<!-- | |
| 2 | + ============================================================ | |
| 3 | + SVGarden — https://www.svgarden.dev | |
| 4 | + Author : Simon-Pierre Boucher | |
| 5 | + Contact: contact@spboucher.ai | |
| 6 | + File : snippets/text-fx/text-scramble-decode.html | |
| 7 | + Desc : Letters cycle through random glyphs, then lock into place | |
| 8 | + ============================================================ | |
| 9 | +--> | |
| 10 | +<!--svgarden | |
| 11 | +title: Scramble decode | |
| 12 | +slug: text-scramble-decode | |
| 13 | +category: text-fx | |
| 14 | +tags: [text, scramble, decode, js] | |
| 15 | +difficulty: advanced | |
| 16 | +techniques: [minimal-js, setInterval-render-loop, prefers-reduced-motion, monospace-stability] | |
| 17 | +how_it_works: > | |
| 18 | + A single interval re-renders the string every 40 ms: characters left of a | |
| 19 | + "locked" cursor show the real message, everything to the right is drawn fresh | |
| 20 | + from a pool of glitch glyphs, and the cursor advances a fraction of a letter | |
| 21 | + per frame so the decode sweeps left to right. The font must be monospace — | |
| 22 | + with proportional glyphs the line would jitter horizontally on every frame. | |
| 23 | + The script checks prefers-reduced-motion and, when set, skips the scramble | |
| 24 | + entirely so motion-sensitive visitors just see the final text; the markup | |
| 25 | + also contains the real sentence, so nothing breaks with JavaScript disabled. | |
| 26 | +customizable: | |
| 27 | + - { var: "--sg-color", label: "Color", type: color, default: "#7F77DD" } | |
| 28 | + - { var: "--sg-size", label: "Font size", type: range, min: 12, max: 40, default: 22, unit: px } | |
| 29 | +created: 2026-08-10 | |
| 30 | +--> | |
| 31 | +<div class="sg-text-scramble-decode" style="--sg-color:#7F77DD; --sg-size:22px;"> | |
| 32 | + <span class="sg-text-scramble-decode-txt">ACCESS GRANTED_</span> | |
| 33 | +</div> | |
| 34 | +<script> | |
| 35 | + document.querySelectorAll('.sg-text-scramble-decode:not([data-sg-init])').forEach(function (root) { | |
| 36 | + root.setAttribute('data-sg-init', ''); | |
| 37 | + var el = root.querySelector('.sg-text-scramble-decode-txt'); | |
| 38 | + var goal = el.textContent; | |
| 39 | + var pool = '!<>-_\\/[]{}=+*^?#@$%&'; | |
| 40 | + if (matchMedia('(prefers-reduced-motion: reduce)').matches) return; | |
| 41 | + (function play() { | |
| 42 | + var locked = 0; | |
| 43 | + var timer = setInterval(function () { | |
| 44 | + var out = goal.slice(0, Math.floor(locked)); | |
| 45 | + for (var i = out.length; i < goal.length; i++) { | |
| 46 | + out += goal[i] === ' ' ? ' ' : pool[(Math.random() * pool.length) | 0]; | |
| 47 | + } | |
| 48 | + el.textContent = out; | |
| 49 | + locked += 0.4; | |
| 50 | + if (locked >= goal.length + 1) { | |
| 51 | + clearInterval(timer); | |
| 52 | + el.textContent = goal; | |
| 53 | + setTimeout(play, 2400); | |
| 54 | + } | |
| 55 | + }, 40); | |
| 56 | + })(); | |
| 57 | + }); | |
| 58 | +</script> | |
| 59 | +<style> | |
| 60 | + .sg-text-scramble-decode { | |
| 61 | + font-family: ui-monospace, 'SF Mono', SFMono-Regular, Menlo, Consolas, monospace; | |
| 62 | + font-size: var(--sg-size); | |
| 63 | + font-weight: 600; | |
| 64 | + color: var(--sg-color); | |
| 65 | + letter-spacing: 0.06em; | |
| 66 | + } | |
| 67 | +</style> | |
added
snippets/text-fx/text-shadow-3d-tilt.html
+68 −0
@@ -0,0 +1,68 @@ | ||
| 1 | +<!-- | |
| 2 | + ============================================================ | |
| 3 | + SVGarden — https://www.svgarden.dev | |
| 4 | + Author : Simon-Pierre Boucher | |
| 5 | + Contact: contact@spboucher.ai | |
| 6 | + File : snippets/text-fx/text-shadow-3d-tilt.html | |
| 7 | + Desc : Extruded pseudo-3D text that tilts in perspective on hover | |
| 8 | + ============================================================ | |
| 9 | +--> | |
| 10 | +<!--svgarden | |
| 11 | +title: 3D extruded tilt | |
| 12 | +slug: text-shadow-3d-tilt | |
| 13 | +category: text-fx | |
| 14 | +tags: [text, 3d, perspective, hover] | |
| 15 | +difficulty: intermediate | |
| 16 | +techniques: [perspective, rotateX-rotateY, --sg-i-index-stagger, grid-area-stacking, brightness-derived-shading] | |
| 17 | +how_it_works: > | |
| 18 | + The extrusion is six copies of the same word stacked in one grid cell — every | |
| 19 | + layer reads its depth index from a --sg-i variable and computes its own offset | |
| 20 | + with translate: calc(var(--sg-i) * var(--sg-depth)), so one rule places the | |
| 21 | + whole stack and the depth becomes a single tunable knob. Shading needs no | |
| 22 | + second color: filter: brightness() derived from the same index darkens deeper | |
| 23 | + layers progressively, which is what sells the solid-block illusion. Hovering | |
| 24 | + (or keyboard-focusing) tips the stack with rotateX/rotateY inside a | |
| 25 | + perspective-carrying wrapper, and because the offsets survive the rotation the | |
| 26 | + extrusion appears genuinely volumetric. | |
| 27 | +customizable: | |
| 28 | + - { var: "--sg-color", label: "Color", type: color, default: "#7F77DD" } | |
| 29 | + - { var: "--sg-size", label: "Font size", type: range, min: 28, max: 90, default: 54, unit: px } | |
| 30 | + - { var: "--sg-depth", label: "Depth", type: range, min: 0.5, max: 3, step: 0.1, default: 1.4, unit: px } | |
| 31 | +created: 2026-08-10 | |
| 32 | +--> | |
| 33 | +<div class="sg-text-shadow-3d-tilt" tabindex="0" aria-label="DEPTH" style="--sg-color:#7F77DD; --sg-size:54px; --sg-depth:1.4px;"> | |
| 34 | + <span class="sg-text-shadow-3d-tilt-stack"> | |
| 35 | + <span class="sg-text-shadow-3d-tilt-layer" style="--sg-i:5" aria-hidden="true">DEPTH</span> | |
| 36 | + <span class="sg-text-shadow-3d-tilt-layer" style="--sg-i:4" aria-hidden="true">DEPTH</span> | |
| 37 | + <span class="sg-text-shadow-3d-tilt-layer" style="--sg-i:3" aria-hidden="true">DEPTH</span> | |
| 38 | + <span class="sg-text-shadow-3d-tilt-layer" style="--sg-i:2" aria-hidden="true">DEPTH</span> | |
| 39 | + <span class="sg-text-shadow-3d-tilt-layer" style="--sg-i:1" aria-hidden="true">DEPTH</span> | |
| 40 | + <span class="sg-text-shadow-3d-tilt-layer" style="--sg-i:0" aria-hidden="true">DEPTH</span> | |
| 41 | + </span> | |
| 42 | +</div> | |
| 43 | +<style> | |
| 44 | + .sg-text-shadow-3d-tilt { | |
| 45 | + display: inline-block; | |
| 46 | + perspective: 550px; | |
| 47 | + cursor: default; | |
| 48 | + } | |
| 49 | + .sg-text-shadow-3d-tilt-stack { | |
| 50 | + display: grid; | |
| 51 | + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; | |
| 52 | + font-size: var(--sg-size); | |
| 53 | + font-weight: 800; | |
| 54 | + letter-spacing: 0.03em; | |
| 55 | + line-height: 1.1; | |
| 56 | + transition: transform 0.45s ease; | |
| 57 | + } | |
| 58 | + .sg-text-shadow-3d-tilt-layer { | |
| 59 | + grid-area: 1 / 1; | |
| 60 | + color: var(--sg-color); | |
| 61 | + translate: calc(var(--sg-i) * var(--sg-depth)) calc(var(--sg-i) * var(--sg-depth)); | |
| 62 | + filter: brightness(calc(1 - var(--sg-i) * 0.13)); | |
| 63 | + } | |
| 64 | + .sg-text-shadow-3d-tilt:hover .sg-text-shadow-3d-tilt-stack, | |
| 65 | + .sg-text-shadow-3d-tilt:focus-visible .sg-text-shadow-3d-tilt-stack { | |
| 66 | + transform: rotateX(18deg) rotateY(-14deg); | |
| 67 | + } | |
| 68 | +</style> | |
added
snippets/text-fx/text-variable-weight-wave.html
+68 −0
@@ -0,0 +1,68 @@ | ||
| 1 | +<!-- | |
| 2 | + ============================================================ | |
| 3 | + SVGarden — https://www.svgarden.dev | |
| 4 | + Author : Simon-Pierre Boucher | |
| 5 | + Contact: contact@spboucher.ai | |
| 6 | + File : snippets/text-fx/text-variable-weight-wave.html | |
| 7 | + Desc : A wave of font-weight rolling across letters via @property | |
| 8 | + ============================================================ | |
| 9 | +--> | |
| 10 | +<!--svgarden | |
| 11 | +title: Variable-weight wave | |
| 12 | +slug: text-variable-weight-wave | |
| 13 | +category: text-fx | |
| 14 | +tags: [text, variable-font, property, stagger] | |
| 15 | +difficulty: advanced | |
| 16 | +techniques: ["@property", font-variation-settings, --sg-i-index-stagger, "@supports"] | |
| 17 | +how_it_works: > | |
| 18 | + Custom properties normally interpolate as raw strings, so animating one just | |
| 19 | + snaps between keyframes. Registering it with @property and syntax:'<number>' | |
| 20 | + tells the engine it is a real number, making it tween smoothly — and here that | |
| 21 | + animated number is piped straight into font-variation-settings "wght", so the | |
| 22 | + glyph skeleton itself thickens and relaxes. Each letter carries its position | |
| 23 | + in a --sg-i variable, and a negative animation-delay computed from it phases | |
| 24 | + the shared keyframe into a traveling wave. Inside @supports the effect is | |
| 25 | + gated: browsers without @property (or without a variable system font) simply | |
| 26 | + show the text at a steady weight. | |
| 27 | +support: > | |
| 28 | + Needs @property and font-variation-settings (Chrome 85+, Safari 16.4+, | |
| 29 | + Firefox 128+) plus a variable system font such as SF Pro or Segoe UI | |
| 30 | + Variable; elsewhere the text renders at a static weight. | |
| 31 | +customizable: | |
| 32 | + - { var: "--sg-color", label: "Color", type: color, default: "#7F77DD" } | |
| 33 | + - { var: "--sg-size", label: "Font size", type: range, min: 20, max: 72, default: 42, unit: px } | |
| 34 | + - { var: "--sg-duration", label: "Speed", type: range, min: 0.8, max: 5, step: 0.1, default: 2, unit: s } | |
| 35 | +created: 2026-08-10 | |
| 36 | +--> | |
| 37 | +<div class="sg-text-variable-weight-wave" aria-label="flexible" style="--sg-color:#7F77DD; --sg-size:42px; --sg-duration:2s;"> | |
| 38 | + <span class="sg-text-variable-weight-wave-ch" style="--sg-i:0" aria-hidden="true">f</span><span class="sg-text-variable-weight-wave-ch" style="--sg-i:1" aria-hidden="true">l</span><span class="sg-text-variable-weight-wave-ch" style="--sg-i:2" aria-hidden="true">e</span><span class="sg-text-variable-weight-wave-ch" style="--sg-i:3" aria-hidden="true">x</span><span class="sg-text-variable-weight-wave-ch" style="--sg-i:4" aria-hidden="true">i</span><span class="sg-text-variable-weight-wave-ch" style="--sg-i:5" aria-hidden="true">b</span><span class="sg-text-variable-weight-wave-ch" style="--sg-i:6" aria-hidden="true">l</span><span class="sg-text-variable-weight-wave-ch" style="--sg-i:7" aria-hidden="true">e</span> | |
| 39 | +</div> | |
| 40 | +<style> | |
| 41 | + @property --sg-tvww-wgt { | |
| 42 | + syntax: '<number>'; | |
| 43 | + inherits: false; | |
| 44 | + initial-value: 400; | |
| 45 | + } | |
| 46 | + .sg-text-variable-weight-wave { | |
| 47 | + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI Variable Text', 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; | |
| 48 | + font-size: var(--sg-size); | |
| 49 | + color: var(--sg-color); | |
| 50 | + letter-spacing: 0.02em; | |
| 51 | + display: inline-flex; | |
| 52 | + } | |
| 53 | + .sg-text-variable-weight-wave-ch { | |
| 54 | + font-weight: 550; | |
| 55 | + } | |
| 56 | + @supports (font-variation-settings: "wght" 500) { | |
| 57 | + .sg-text-variable-weight-wave-ch { | |
| 58 | + animation: sg-text-variable-weight-wave-roll var(--sg-duration) ease-in-out infinite; | |
| 59 | + animation-delay: calc(var(--sg-i) * var(--sg-duration) / -8); | |
| 60 | + font-variation-settings: "wght" var(--sg-tvww-wgt); | |
| 61 | + font-weight: normal; | |
| 62 | + } | |
| 63 | + } | |
| 64 | + @keyframes sg-text-variable-weight-wave-roll { | |
| 65 | + 0%, 100% { --sg-tvww-wgt: 260; } | |
| 66 | + 50% { --sg-tvww-wgt: 840; } | |
| 67 | + } | |
| 68 | +</style> | |
| 69 | ||