Type Morph
`<pura-type-morph>` makes the type itself the animation. The slotted text physically thickens (`wght`) and widens (`wdth`) as it scrolls. The axes ride the native `font-weight` and `font-stretch` properties, not `font-variation-settings`: both are natively animatable and, crucially, always re-rasterize the glyph (Chromium will not re-render a glyph when an *animated* value reaches `font-variation-settings` through `var()`), so a transition or a scroll-driven keyframe on those two properties is what actually moves the type. They inherit through the flat tree, so the slotted light-DOM text picks them up from `:host`. The default `trigger="scrub"` binds the axes 1:1 to a scroll-driven timeline (`animation-timeline: view()` or `scroll()`), so the letters morph as you scroll, with no per-frame JS. `trigger="view"` morphs once when scrolled into view (eased by the spring primitive); `trigger="load"` morphs once on connect. Needs a variable font with the relevant axes; modern system UI fonts expose `wght` (`wdth` needs a font that carries it, e.g. Roboto Flex). SSR and reduced motion are safe: before JS the text sits at its `from` axes and is fully readable, the scrub block is gated behind `prefers-reduced-motion: no-preference`, and the reduced-motion path lands the text at its destination weight. Each instance registers in `window.__puraTypeMorphs` by `data-pura-id` with `{ from, to, replay }`.
Preview
<!-- default: scroll and the letters morph -->
<pura-type-morph from-wght="300" to-wght="900">
Scroll to make me heavy.
</pura-type-morph>
<!-- one-shot on view, with a width squeeze -->
<pura-type-morph trigger="view" from-wght="700" to-wght="300"
from-wdth="75" to-wdth="125" preset="wobbly">
Lighter and wider.
</pura-type-morph>
<!-- scrub against the page scroll container -->
<pura-type-morph timeline="scroll" range="cover 0% cover 100%">
Tied to the page.
</pura-type-morph> Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
trigger | "scrub" | "view" | "load" | scrub | scrub ties the axes 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-wght | number | 400 | Starting weight axis. |
to-wght | number | 800 | Ending weight axis. |
from-wdth | number | 100 | Starting width axis (percent). |
to-wdth | number | 100 | Ending width axis (percent). |
range | string | cover 0% cover 50% | animation-range for the scrub timeline. Default completes the morph as the word 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/type-morph.js"></script> import "./pura/lib/type-morph.js";