Morphing SVG Preloader

A glowing SVG form morphs circle → square → blob → star → mark while a hairline outline traces each new shape and a percentage climbs; at 100% the form scales 40× to fill the viewport and fades into the page.

Loading

AI prompt

Recreate this effect with any AI tool

Paste into Claude, Cursor, v0, Lovable or Bolt. 1486 words of exact values — rewritten live as you tune the Playground.

prompt.md · Morphing SVG Preloader
### EFFECT: Morphing SVG Preloader
**What it does (visual):** The page opens on a solid dark screen. In the centre an off-white circle (about 26vw wide, max 220px) pops in from 60% scale with a slight overshoot, haloed by a soft glow in `#d4ff3a` (18px inner radius, a wider ~3× outer halo). A 1px `#d4ff3a` line sweeps once around a ring just outside the circle and vanishes. Beneath the form a large light serif percentage reads "0%" with the mono label "01 — CIRCLE" under it. Then, every 0.9 seconds, the filled form melts into the next shape — circle → rounded square → organic blob → soft four-point star → a leaf-like logo mark — each morph easing with `power2.inOut`. At the start of each step the outline (already in the shape of the *next* form) draws itself clockwise around the ring and then draws itself away again, so it always traces the form the fill is becoming. The percentage climbs 0 → 100 across the whole morph sequence (integer steps, tabular digits) and the label updates per step ("02 — SQUARE" … "05 — MARK"). At 100% the glow switches off, the outline disappears, the percentage fades up and away, and the mark swells 40× in 0.9s (slow then violently fast) until it covers the viewport; the loader then fades out over 0.6s revealing a page whose nav bar, accent eyebrow, giant serif headline "Shape is a verb.", paragraph and Replay button rise in with a stagger. Replay runs it all again.

**Tech stack:**
- GSAP 3.13+ with MorphSVGPlugin and DrawSVGPlugin (both free since 3.13).
- CDN: `gsap.min.js`, `MorphSVGPlugin.min.js`, `DrawSVGPlugin.min.js` from `https://cdn.jsdelivr.net/npm/gsap@3/dist/`.
- npm: `import gsap from "gsap"; import { MorphSVGPlugin } from "gsap/MorphSVGPlugin"; import { DrawSVGPlugin } from "gsap/DrawSVGPlugin"; gsap.registerPlugin(MorphSVGPlugin, DrawSVGPlugin);`
- Fonts: a variable serif for the percentage and headline (demo: Fraunces 300), a mono for labels.

**HTML structure:**
```html
<div class="msp">
  <div class="msp__loader" aria-hidden="true">
    <svg class="msp__svg" viewBox="0 0 200 200" width="200" height="200">
      <defs>
        <path id="msp-s1" d="M100,20 C144.2,20 180,55.8 180,100 C180,144.2 144.2,180 100,180 C55.8,180 20,144.2 20,100 C20,55.8 55.8,20 100,20 Z"/>
        <path id="msp-s2" d="M62,30 H138 C155.7,30 170,44.3 170,62 V138 C170,155.7 155.7,170 138,170 H62 C44.3,170 30,155.7 30,138 V62 C30,44.3 44.3,30 62,30 Z"/>
        <path id="msp-s3" d="M104,22 C146,16 182,54 178,98 C174,142 148,184 102,180 C56,176 18,146 22,100 C26,54 62,28 104,22 Z"/>
        <path id="msp-s4" d="M100,18 C110,66 134,90 182,100 C134,110 110,134 100,182 C90,134 66,110 18,100 C66,90 90,66 100,18 Z"/>
        <path id="msp-s5" d="M38,162 C38,86 92,38 162,38 C162,114 108,162 38,162 Z"/>
      </defs>
      <g class="msp__ring" transform="translate(100 100) scale(1.16) translate(-100 -100)">
        <path class="msp__outline" d="(same d as #msp-s1)"/>
      </g>
      <path class="msp__shape" d="(same d as #msp-s1)"/>
    </svg>
    <div class="msp__meta">
      <p class="msp__pct"><span class="msp__pct-num">0</span><span class="msp__pct-sign">%</span></p>
      <p class="msp__step">01 — Circle</p>
    </div>
  </div>
  <main class="msp__page">
    <header class="msp__nav"><span class="msp__brand">Forma</span><nav class="msp__links"><a href="#">Typefaces</a><a href="#">Studio</a><a href="#">Journal</a></nav></header>
    <div class="msp__hero">
      <p class="msp__eyebrow">Type foundry &amp; motion studio</p>
      <h1 class="msp__title">Shape is a verb.</h1>
      <p class="msp__sub">Five letterforms, one gesture.</p>
      <button class="msp__replay" type="button">Replay preloader</button>
    </div>
  </main>
</div>
```
The five `d` strings above are the exact morph targets (all cubic Béziers in a 200×200 box; point counts differ — MorphSVG's default `shapeIndex: "auto"` handles that).

**CSS requirements:**
- `.msp__loader`: `position: fixed; inset: 0; z-index: 10; background: var(--bg, #0a0a0a); display: grid; place-content: center; justify-items: center; gap: clamp(1.5rem, 4vh, 3rem); pointer-events: none;`
- `.msp__svg`: `width: clamp(140px, 26vw, 220px); height: auto; overflow: visible; will-change: transform;`
- `.msp__shape`: `fill: var(--text, #ededed); filter: drop-shadow(0 0 18px color-mix(in srgb, var(--accent, #d4ff3a) 55%, transparent)) drop-shadow(0 0 calc(18px * 3.1) color-mix(in srgb, var(--accent) 28%, transparent));`
- `.msp__outline`: `fill: none; stroke: var(--accent); stroke-width: 1.1; stroke-linecap: round;` — the parent `<g>` is scaled 1.16 about the centre so the ring sits just outside the fill.
- `.msp__pct`: display serif, `font-weight: 300; font-size: clamp(2.4rem, 6vw, 4.2rem); line-height: 1; letter-spacing: -0.04em; font-variant-numeric: tabular-nums;` — `.msp__pct-sign { font-size: .45em; color: var(--muted); vertical-align: .9em }`.
- `.msp__step`: mono, `11px`, uppercase, `letter-spacing: .18em`, muted, `margin-top: .9rem`.
- `.msp__page`: `min-height: 100vh; display: grid; grid-template-rows: auto 1fr; padding: clamp(1.25rem, 3vw, 2.5rem) 6vw clamp(3rem, 8vw, 6rem);` — `.msp__nav` is a flex row with a `1px solid var(--line)` bottom border; `.msp__title` is `clamp(3rem, 10vw, 10rem)`, `line-height: .9`, `letter-spacing: -0.045em`; `.msp__eyebrow` is mono 11px in `--accent`.
- `.msp__nav, .msp__eyebrow, .msp__title, .msp__sub, .msp__replay` start at `opacity: 0` (JS reveals them).

**Animation spec (exact values):**
Create `const counter = { v: 0 }` and one `tl = gsap.timeline()`. Constants: `STEP = 0.9`, `INTRO = 0.7`, `MORPH_END = INTRO + 4 × STEP` (4.3 at the default 0.9), `EXIT_AT = MORPH_END + 0.25` (4.55 at default), `EXIT_DURATION = 0.9`.
1. Reset block at 0 (so `restart()` is clean): `tl.set('.msp__loader', { autoAlpha: 1 }).set('.msp__svg', { scale: 1, transformOrigin: '50% 50%' }).set('.msp__shape', { morphSVG: '#msp-s1', clearProps: 'filter', scale: 0.6, opacity: 0, transformOrigin: '50% 50%' }).set('.msp__outline', { morphSVG: '#msp-s1', drawSVG: '0% 0%', autoAlpha: 1 }).set('.msp__meta', { opacity: 1, y: 0 })` and a `.call()` that writes "01 — Circle".
2. Intro: `tl.to('.msp__shape', { scale: 1, opacity: 1, duration: 0.7, ease: 'back.out(1.6)' }, 0)`, `tl.to('.msp__outline', { drawSVG: '0% 100%', duration: 0.45, ease: 'power2.out' }, 0.05)`, `tl.to('.msp__outline', { drawSVG: '100% 100%', duration: 0.4, ease: 'power2.in' }, 0.45)`.
3. Counter: `tl.fromTo(counter, { v: 0 }, { v: 100, duration: MORPH_END, ease: 'power1.inOut', snap: { v: 1 }, onUpdate: () => pctNum.textContent = String(counter.v) }, 0)`.
4. Morph steps — for `i = 0..3`, `t = INTRO + i × STEP`, `target = '#msp-s' + (i + 2)`, `trace = min(0.5, STEP × 0.56)` (0.5 at default; shrinks with a short step so the sweep always ends before the next swap):
   - fill: `tl.to('.msp__shape', { morphSVG: target, duration: 0.9, ease: 'power2.inOut' }, t)`
   - outline swap while invisible: `tl.set('.msp__outline', { morphSVG: target, drawSVG: '0% 0%' }, t)`
   - trace in: `tl.to('.msp__outline', { drawSVG: '0% 100%', duration: trace, ease: 'power2.out' }, t)`
   - trace out: `tl.to('.msp__outline', { drawSVG: '100% 100%', duration: trace × 0.84, ease: 'power2.in' }, t + trace × 0.96)` (0.42s at +0.48 by default)
   - label: `tl.call(() => stepLabel.textContent = '0' + (i + 2) + ' — ' + name, null, t)` with names Square, Blob, Star, Mark.
5. Exit at `EXIT_AT`: `tl.set('.msp__shape', { filter: 'none' }, EXIT_AT)`, `tl.set('.msp__outline', { autoAlpha: 0 }, EXIT_AT)`, `tl.to('.msp__meta', { opacity: 0, y: -12, duration: 0.35, ease: 'power2.in' }, EXIT_AT)`, `tl.to('.msp__svg', { scale: 40, duration: 0.9, ease: 'expo.in' }, EXIT_AT)`.
6. Reveal: `tl.to('.msp__loader', { autoAlpha: 0, duration: 0.6, ease: 'power2.out' }, EXIT_AT + EXIT_DURATION)` and `tl.fromTo(['.msp__nav', '.msp__eyebrow', '.msp__title', '.msp__sub', '.msp__replay'], { y: 28, opacity: 0 }, { y: 0, opacity: 1, duration: 1, ease: 'power3.out', stagger: 0.08 }, EXIT_AT + EXIT_DURATION - 0.05)`.
Total ≈ EXIT_AT + EXIT_DURATION + 1.3 (≈6.7s at defaults).

**Interaction:** Purely time-based; no scroll or cursor dependency. `.msp__replay` click → `tl.restart()`. Nav links lighten on hover (CSS, 0.2s).

**Responsive behavior:** Everything is fluid (`clamp()` sizes, `26vw` SVG); nothing changes structurally under 768px except the third nav link is hidden. The exit scale always covers the viewport as long as `140px × 40` exceeds the longest viewport edge (5600px at the default 40). `gsap.matchMedia()` is not required; if the headline wraps to two lines on phones that is intended.

**Accessibility:** The loader is `aria-hidden="true"` and `pointer-events: none`. Under `prefers-reduced-motion: reduce` do not run the timeline: `display: none` the loader, show the page content at full opacity and hide Replay (do it in CSS and JS). The page content remains in the DOM throughout so screen readers are never blocked by the loader. Replay has a visible focus ring (`outline: 2px solid var(--accent); outline-offset: 3px`).

**Performance rules:** Only `d` (morph), `stroke-dasharray/offset` (draw), `transform` and `opacity` change. Set `filter: none` on the fill *before* the 40× scale — a `drop-shadow` on a multi-thousand-pixel element tanks the frame rate. Never animate `drawSVG` on a path whose `d` is mid-morph (DrawSVG measures length at tween start). Everything is inside `gsap.context()` and reverted on unmount (`ctx.revert()` restores the original `d` attributes too). The host pauses `gsap.globalTimeline` when the demo is off-screen.

**Customization knobs:**
- `STEP` (0.9) — seconds per morph; `INTRO` (0.7).
- The five `d` strings — swap `#msp-s5` for your real logo path (keep it inside the 200×200 box).
- Morph ease `power2.inOut`; add `type: 'rotational'` to `morphSVG` for blobbier transitions.
- Glow radius (18px inner, ×3.1 outer) and colour (`--accent`: #d4ff3a); ring offset via the group's `scale(1.16)`.
- Exit scale (40), exit duration (0.9s) and ease (`expo.in`).

**Acceptance criteria:**
- [ ] The form passes through all five shapes in order, each morph 0.9s `power2.inOut`, with no flicker or path "flip".
- [ ] The accent outline is never visible at a step boundary and always traces the shape the fill is turning into.
- [ ] Percentage reaches exactly 100 as the fifth shape lands (`0.7 + 4 × 0.9`s) and the step label reads "05 — MARK".
- [ ] The glow disappears before the scale-up; the form fully covers the viewport before the loader fades.
- [ ] Page content rises in with a 0.08s stagger; Replay restarts cleanly from the circle at 0%.
- [ ] Reduced-motion users see the page immediately; no console errors.

Source

Full source code

<div class="msp">
  <div class="msp__loader" aria-hidden="true">
    <svg class="msp__svg" viewBox="0 0 200 200" width="200" height="200">
      <defs>
        <path id="msp-s1" d="M100,20 C144.2,20 180,55.8 180,100 C180,144.2 144.2,180 100,180 C55.8,180 20,144.2 20,100 C20,55.8 55.8,20 100,20 Z"/>
        <path id="msp-s2" d="M62,30 H138 C155.7,30 170,44.3 170,62 V138 C170,155.7 155.7,170 138,170 H62 C44.3,170 30,155.7 30,138 V62 C30,44.3 44.3,30 62,30 Z"/>
        <path id="msp-s3" d="M104,22 C146,16 182,54 178,98 C174,142 148,184 102,180 C56,176 18,146 22,100 C26,54 62,28 104,22 Z"/>
        <path id="msp-s4" d="M100,18 C110,66 134,90 182,100 C134,110 110,134 100,182 C90,134 66,110 18,100 C66,90 90,66 100,18 Z"/>
        <path id="msp-s5" d="M38,162 C38,86 92,38 162,38 C162,114 108,162 38,162 Z"/>
      </defs>
      <g class="msp__ring" transform="translate(100 100) scale(1.16) translate(-100 -100)">
        <path class="msp__outline" d="M100,20 C144.2,20 180,55.8 180,100 C180,144.2 144.2,180 100,180 C55.8,180 20,144.2 20,100 C20,55.8 55.8,20 100,20 Z"/>
      </g>
      <path class="msp__shape" d="M100,20 C144.2,20 180,55.8 180,100 C180,144.2 144.2,180 100,180 C55.8,180 20,144.2 20,100 C20,55.8 55.8,20 100,20 Z"/>
    </svg>
    <div class="msp__meta">
      <p class="msp__pct"><span class="msp__pct-num">0</span><span class="msp__pct-sign">%</span></p>
      <p class="msp__step">01 — Circle</p>
    </div>
  </div>

  <main class="msp__page">
    <header class="msp__nav">
      <span class="msp__brand">Forma</span>
      <nav class="msp__links" aria-label="Site"><a href="#">Typefaces</a><a href="#">Studio</a><a href="#">Journal</a></nav>
    </header>
    <div class="msp__hero">
      <p class="msp__eyebrow">Type foundry &amp; motion studio</p>
      <h1 class="msp__title">Shape is a verb.</h1>
      <p class="msp__sub">Five letterforms, one gesture. Forma draws type that was designed to move — every glyph has a morph target.</p>
      <button class="msp__replay" type="button">Replay preloader</button>
    </div>
  </main>
</div>