SVGarden 🌱
A massive, searchable bank of SVG + CSS animations with copy-ready code snippets.
Production: https://www.svgarden.dev · Author: Simon-Pierre Boucher — contact@spboucher.ai
Every animation is one self-contained .html file: paste it into a blank page and it works.
No frameworks, no CDNs, no build step required by the snippets themselves — and each one
ships with a short "How it works" lesson. The site is static (Astro), searched client-side
(lazy Fuse.js), highlighted at build time (Shiki), and customized live through CSS custom
properties.
📊 Metrics
The bank
| Metric | Value |
|---|---|
| 🟣 Snippets | 74 |
| 🗂 Categories | 14 |
| 🧪 Distinct techniques taught | 177 |
| ⚡ JavaScript-free snippets | 67 / 74 (90 %) |
🧭 Snippets with a browser-support caveat (support) |
13 |
| 📏 Average snippet size | 57 lines (max 116, hard cap 150) |
| 🧩 Pages generated per build | 84 |
Difficulty
| Level | Count | Share | |
|---|---|---|---|
| 🟢 | beginner | 9 | 12 % |
| 🟣 | intermediate | 27 | 36 % |
| 🔴 | advanced | 38 | 51 % |
Categories
| Category | Count | Headline techniques |
|---|---|---|
🔘 buttons |
8 | goo filter, clipPath sweeps, @property hue trace, linear() springs, multi-stage morph |
🎛 filters |
8 | feTurbulence, feDisplacementMap, goo matrix, feMorphology, feDropShadow stacks, backdrop-filter |
🖼 backgrounds |
7 | mix-blend-mode aurora, SMIL topo morphs, turbulence rain refraction, seamless tiling |
🌀 loaders |
6 | dasharray chase, counter-rotation, choreographed keyframes |
✨ text-fx |
6 | mask knockout, variable-font weight waves, liquid clipPath fill, scramble decode |
📜 scroll |
6 | animation-timeline: scroll() / view(), animation-range, sticky scenes |
📈 charts |
6 | conic-gradient() + @property sweeps, live polyline updates, --sg-i cascades |
🎚 interactive |
6 | :checked scenes, reversed-radio rating, <details> accordion, pseudo-3D tilt |
🛰 motion-path |
5 | offset-path, animateMotion rotate="auto", keyPoints/keyTimes pacing |
✏️ stroke-draw |
4 | pathLength normalization, staggered dashoffset |
🖱 hover |
4 | transform-box, transition choreography |
⏲ gauges |
3 | dashoffset data-mapping, calc() needle angles |
🔤 text |
3 | textPath, SMIL startOffset, glyph stroke tracing |
🫧 morph |
2 | SMIL d-morphing, CSS d: with fallback |
Quality gates (every build)
| Gate | Status |
|---|---|
validate-snippets.mjs (headers, frontmatter, scoping, self-containment, size) |
🟢 74/74 |
npm run build warnings |
🟢 0 |
| Lighthouse performance / accessibility (home + detail) | 🟢 100 / 100 |
| Paste-test (copy output → blank .html) | 🟢 verified per batch |
Quick start
npm install
npm run dev # local dev server on :4321
npm run build # validate snippets + build static site into dist/
npm run serve # serve dist/ with the production Express server (:4321, /healthz)Adding a snippet
npm run new # interactive scaffolder → snippets/<category>/<slug>.html
npm run validate # the same gate the build runsOne file = one animation. The gallery, detail page, category pages, search index and tag filters all regenerate at build time — never edit generated pages.
Snippet hard rules (enforced by scripts/validate-snippets.mjs, which fails the build):
- SVGarden author header at the top (attribution travels with every copy/download);
<!--svgarden ... -->YAML frontmatter (title, slug, category, tags, difficulty, techniques, how_it_works, customizable, created, and optionalsupport);- self-contained (no external assets), all classes prefixed
sg-<slug>, - customizable values as
--sg-*custom properties declared on the root element, - ≤ 150 lines,
jstag iff the snippet contains a<script>, - every
<svg>isaria-hidden="true"orrole="img"+<title>; - cutting-edge CSS (
@property,animation-timeline,:has(),backdrop-filter,linear(), CSSd) must sit behind@supportswith a graceful static fallback, and declare asupport: <one-line caveat>frontmatter field — the detail page renders it as a browser-support note.
Architecture
| Piece | Where |
|---|---|
| Snippet bank | snippets/<category>/<slug>.html |
| Parser / single source of truth | src/lib/snippets.mjs |
| Site (Astro, static output) | src/pages, src/components, src/layouts |
| Search index endpoint | src/pages/search-index.json.js (Fuse.js, lazy-loaded client-side) |
Build info for /healthz |
src/pages/build-info.json.js |
| Production server | server.mjs (Express, gzip/brotli, immutable caching for hashed assets) |
| Process manager | ecosystem.config.cjs (pm2: svgarden-web + svgarden-tunnel) |
| Deploy | scripts/deploy.sh |
Deployment (node M3U96b + ngrok)
The site runs on cluster node M3U96b, served at 127.0.0.1:4321 and exposed publicly
through an ngrok tunnel at https://www.svgarden.dev.
./scripts/deploy.shThe script syncs the source to the node (via git pull once a git remote exists on the
node; via rsync until then), runs npm ci + npm run build, restarts both pm2 apps,
then health-checks http://127.0.0.1:4321/healthz and https://www.svgarden.dev/healthz.
One-time ngrok requirements (operator steps)
- ngrok installed and authenticated on the node (
ngrok config add-authtoken <token>). Never commit or share the token. www.svgarden.devreserved as a custom domain in the ngrok dashboard (Universal Gateway → Domains).- DNS: a
CNAMEforwww.svgarden.devpointing at the target ngrok gives you when reserving the domain. - Tunnel definition in the node's ngrok config (
~/Library/Application Support/ngrok/ngrok.ymlon macOS — add the block, don't overwrite the file):
tunnels:
svgarden:
proto: http
addr: 4321
domain: www.svgarden.devpm2 keeps both alive across reboots (pm2 save + pm2 startup): svgarden-web runs
server.mjs, svgarden-tunnel runs ngrok start svgarden --log=stdout.
© Simon-Pierre Boucher — contact@spboucher.ai — svgarden.dev