Variable Font Morph
Sweep a variable font's weight and width axes across a line, one character at a time. Install with npx remotion-ui@latest add variable-font-morph.
PrimitiveAdvanced
$ npx remotion-ui@latest add variable-font-morphA weight wave travelling through a headline.
import { VariableFontMorph } from "@/remotion/primitives/variable-font-morph";
<VariableFontMorph
text="Weight in motion"
weight={[200, 900]}
width={[85, 115]}
oscillate
periodInFrames={46}
/>One position, every axis
The axis position is a single 0–1 value per character, spent on every requested
axis at once. A real variable face moves wght and wdth together; driving
them from separate clocks makes the type look broken rather than variable.
weight, width and slant are the registered axes. Anything else goes
through axes by four-letter tag:
<VariableFontMorph text="Optical" axes={{ opsz: [14, 96] }} />Oscillate, or arrive
By default the axes ramp from from to to with the entrance and stay there.
oscillate keeps them travelling between the two ends forever, offset per
character by phaseStep so the wave moves along the line.
Reserved advance widths
Weight and width both change a glyph's advance, so a centred line re-centres
itself every frame and the whole string jitters horizontally as the wave passes
through it. reserveSpace, on by default, lays each character out in an
inline-grid cell sized by a hidden copy at the heaviest, widest setting, and
centres the animated glyph inside that fixed cell. The line cannot move. Turn it
off if you want the type to breathe.
Load a real variable face
font-variation-settings does nothing on a face without those axes, so the
wght and wdth values are also written to font-weight and font-stretch.
A family shipping discrete weights therefore steps between them instead of
gliding (visibly coarser, but never frozen), and it is not what this component
is for. Load a variable face at module scope and pass its family:
import { loadFont } from "@remotion/google-fonts/Inter";
const { fontFamily } = loadFont("normal", {
weights: ["100", "200", "300", "400", "500", "600", "700", "800", "900"],
subsets: ["latin"],
});
<VariableFontMorph text="Weight in motion" fontFamily={fontFamily} />Google serves Inter as a single variable woff2 (every one of those nine weight
URLs is the same file), so the wght axis has somewhere continuous to travel.
One 0–1 position drives every axis at once. On a static face the values fall back to `font-weight`/`font-stretch`, which steps rather than glides.
Agent notes
Install first, then import the copied source component locally. AI guide →
@/remotion/primitives/variable-font-morph- Use when: frame-level motion primitives and reusable animation wrappers.
- Customize: text, weight, width, slant, plus copied source for timing, layout, colors, and typography.
- Rule: do not import this component from the
remotion-uinpm package; it is copied into your project.
Usage
import { VariableFontMorph } from "@/remotion/primitives/variable-font-morph";
<VariableFontMorph text="Weight in motion" weight={[200, 900]} oscillate />API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
| text | string | - | The line whose axes are swept. |
| weight | [number, number] | [200, 800] | `wght` axis. Also mirrored onto `font-weight`. |
| width | [number, number] | - | `wdth` axis in percent. Also mirrored onto `font-stretch`. |
| slant | [number, number] | - | `slnt` axis in degrees. Negative leans right, per the spec. |
| axes | Record<string, [number, number]> | - | Any further axes by four-letter tag, e.g. `{ opsz: [14, 96] }`. |
| oscillate | boolean | false | Keep travelling between the two ends instead of landing on `to`. |
| periodInFrames | number | 60 | Frames for one there-and-back when oscillating. |
| phaseStep | number | 0.55 | Radians of offset per character along the wave. |
| staggerInFrames | number | 2 | Frames between one character starting and the next. |
| durationInFrames | number | 20 | Length of one character's ramp. |
| delayInFrames | number | 0 | Frames before the first character starts. |
| fontFamily | string | - | Pass a variable face here: the fallback only steps between static weights. |
| reserveSpace | boolean | true | Lay each glyph out in a cell sized by a hidden copy at the heaviest, widest setting. Weight changes advance widths, so without it a centred line re-centres itself every frame. |