RemotionUI

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

variable-font-morph30fps · 960×540
Install
$ npx remotion-ui@latest add variable-font-morph

A 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 →

Import
@/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-ui npm package; it is copied into your project.

Usage

Example
import { VariableFontMorph } from "@/remotion/primitives/variable-font-morph"; <VariableFontMorph text="Weight in motion" weight={[200, 900]} oscillate />

API Reference

PropTypeDescription
textstringThe line whose axes are swept.
weight[number, number]`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.
axesRecord<string, [number, number]>Any further axes by four-letter tag, e.g. `{ opsz: [14, 96] }`.
oscillatebooleanKeep travelling between the two ends instead of landing on `to`.
periodInFramesnumberFrames for one there-and-back when oscillating.
phaseStepnumberRadians of offset per character along the wave.
staggerInFramesnumberFrames between one character starting and the next.
durationInFramesnumberLength of one character's ramp.
delayInFramesnumberFrames before the first character starts.
fontFamilystringPass a variable face here: the fallback only steps between static weights.
reserveSpacebooleanLay 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.

On this page