Morph
`<pura-morph>` bends one SVG shape into another as you scroll. The native `d` geometry property interpolates between two `path()` values, baked into a scroll-driven keyframe as literal strings (never routed through a `var()` into geometry, which would not re-paint). The default `trigger="scrub"` ties the path 1:1 to a scroll-driven timeline (`animation-timeline: view()` or `scroll()`), so the shape morphs under the scrollbar with no per-frame JS and no flubber dependency. `trigger="view"` morphs once when scrolled into view (eased by the spring primitive); `trigger="load"` morphs once on connect. The one rule: `from` and `to` must share command structure, the same count and order of `M`/`L`/`C`/`Z` commands. A square and a diamond are both `M L L L Z`, so they interpolate; a triangle and a pentagon do not, and snap. SSR and reduced motion are safe: before JS the shape sits fully painted at its `from` path, the scrub block is gated behind `prefers-reduced-motion: no-preference`, and the reduced-motion path lands the shape at its destination. Each instance registers in `window.__puraMorphs` by `data-pura-id` with `{ from, to, replay }`.
Preview
<!-- default: scroll and the square becomes a diamond -->
<pura-morph style="width: 160px; height: 160px;"></pura-morph>
<!-- custom matched paths, one-shot on view -->
<pura-morph trigger="view"
from="M 10 50 L 50 10 L 90 50 L 50 90 Z"
to="M 10 30 L 50 50 L 90 30 L 50 90 Z"
fill="#2563eb" preset="wobbly"
style="width: 200px; height: 200px;"></pura-morph>
<!-- scrub against the page scroll container -->
<pura-morph timeline="scroll" range="cover 0% cover 100%"
from="M 20 20 L 80 20 L 80 80 L 20 80 Z"
to="M 50 14 L 86 50 L 50 86 L 14 50 Z"
style="width: 160px; height: 160px;"></pura-morph> Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
trigger | "scrub" | "view" | "load" | scrub | scrub ties the path 1:1 to a scroll-driven timeline; view morphs once when scrolled into view; load morphs once on connect. |
timeline | "view" | "scroll" | view | Scrub only. view maps the element's own view progress; scroll maps the nearest scroll container. |
from | string | square | Starting SVG path d-string. Must share command structure with `to`. |
to | string | diamond | Ending SVG path d-string. Same count and order of commands as `from`, or it snaps instead of morphing. |
viewbox | string | 0 0 100 100 | SVG viewBox the paths are drawn in. |
fill | string | currentColor | Shape fill. |
stroke | string | none | Shape stroke color. |
stroke-width | number | 0 | Shape stroke width, in viewBox units. |
range | string | cover 0% cover 50% | animation-range for the scrub timeline. Default completes the morph as the shape reaches viewport center, then holds. |
preset | "default" | "gentle" | "wobbly" | "stiff" | "slow" | "snappy" | default | Spring easing for view/load triggers. Or set stiffness/damping/mass directly. |
Installation
pura is copy-paste and dependency-free. Load the whole library, or just this component.
<link rel="stylesheet" href="/pura/tokens.css">
<script type="module" src="/pura/lib/morph.js"></script> import "./pura/lib/morph.js";