← Back to gallery
CSS

Variable Font Weight Morph

Live text morphs through font-variation-settings axes (wght, wdth, slnt) inside a reserved track — no scale transforms, no layout shift. Three axis programs: a weight pulse, an elastic width breathe, and a multi-axis morph.

variable-fontfont-variation-settingswghtwdthslntno-layout-shiftprefers-reduced-motion

Variable typography · axis morph

Variable Font Weight Morph

Font variation settings interpolate weight, width, and slant inside a reserved text track so morphing type does not cause nearby layout shift. Three variants demonstrate a wght pulse, a wdth breathing pass, and an editorial slnt + wght combo — each on a fixed inline track.

wght axis · stable track

Weight Pulse

A boldness morph cycles the weight axis from regular up to heavy and back. The preview stage reserves enough inline width for the heaviest frame so neighboring content never shifts as the weight changes.

  • wght
  • font-variation
  • stable width

wdth axis · no scaleX

Width Breathing

Width variation breathes through font-variation-settings rather than geometric `transform: scaleX()` — the glyph design itself widens, no distortion. The reserved text track prevents axis changes from pushing neighboring content around.

  • wdth
  • no scale
  • overflow guard

slnt axis · controlled tilt

Editorial Slant

A restrained slant + weight morph creates motion without distorting glyph geometry. Slant values stay conservative; strong tilt can degrade reading even when the axis is technically supported.

  • slnt
  • typography
  • readability

Variable font inspector

Weight Pulse

  • wght
  • font-variation
  • stable width

A boldness morph cycles the weight axis from regular up to heavy and back. The preview stage reserves enough inline width for the heaviest frame so neighboring content never shifts as the weight changes.

Helped you ship something? 🐟 Send my cat a churu

.varfont-word {
  display: inline-block;
  /* Reserve enough inline space for the heaviest / widest frame so
     the morph never shifts neighboring layout. */
  min-width: min(100%, 12ch);
  padding: 0 0.08em;
  overflow: hidden;
  color: #fff7ed;
  font-variation-settings: "wght" 360, "wdth" 100, "slnt" 0;
  text-shadow: 0 0 22px rgba(251, 191, 36, 0.13);
  animation: varfont-axis 3.00s ease-in-out infinite alternate;
}

@keyframes varfont-axis {
  from { font-variation-settings: "wght" 360, "wdth" 100, "slnt" 0; }
  to   { font-variation-settings: "wght" 880, "wdth" 100, "slnt" 0; }
}

@media (prefers-reduced-motion: reduce) {
  .varfont-word {
    animation: none;
    /* Pin to a balanced static axis state for readability. */
    font-variation-settings: "wght" 360, "wdth" 100, "slnt" 0;
  }
}

How to make this

A variable font morph animates real font axes inside a reserved text track, so weight, width, or slant can move without pushing nearby layout.

html
1<p class="varfont-demo">  <span class="varfont-demo__label">Current mode</span>  <span class="varfont-demo__word">Axis Morph</span></p> <style>.varfont-demo {  display: grid;  gap: .5rem;  width: min(100%, 18rem);  margin: 0;  color: #f8fafc;13  font-family: "Roboto Flex", ui-sans-serif, system-ui, sans-serif;}.varfont-demo__label {  color: #93c5fd;  font: 700 .72rem/1.2 ui-sans-serif, system-ui;  letter-spacing: .08em;  text-transform: uppercase;}.varfont-demo__word {22  display: inline-block;  width: min(100%, 14ch);  overflow: hidden;  color: #fff7ed;  font-size: clamp(2rem, 9vw, 4rem);  font-weight: 430;28  font-variation-settings: "wght" 430, "wdth" 96, "slnt" 0;  text-shadow: 0 0 24px rgba(251, 191, 36, .2);  animation: varfont-morph-recipe-axis 3s ease-in-out infinite alternate;}32@keyframes varfont-morph-recipe-axis {  from {    font-weight: 430;    font-variation-settings: "wght" 430, "wdth" 96, "slnt" 0;  }  to {    font-weight: 860;    font-variation-settings: "wght" 860, "wdth" 112, "slnt" -6;  }}42@supports not (font-variation-settings: normal) {  .varfont-demo__word { font-weight: 760; }}45@media (prefers-reduced-motion: reduce) {  .varfont-demo__word {    animation: none;    font-weight: 650;    font-variation-settings: "wght" 650, "wdth" 100, "slnt" 0;  }}</style>

Annotated snippet

  1. Line 1The animated word is still live text, not an image or canvas. It remains selectable, copyable, and exposed as ordinary text content.
    PitfallWhy is my variable font morph not visibly changing?

    The loaded font may not expose the axis you are animating. Check the font file or provider options for wght, wdth, slnt, opsz, or other axes, and test the exact browser-rendered font stack.

  2. Line 13The font stack must include a variable font with the axes you animate. Weight usually works first; width and slant need a font that actually exposes wdth and slnt.
    PitfallWhy is my variable font morph not visibly changing?

    The loaded font may not expose the axis you are animating. Check the font file or provider options for wght, wdth, slnt, opsz, or other axes, and test the exact browser-rendered font stack.

  3. Line 22Reserve the widest expected inline track before animation starts. Axis changes can alter glyph metrics, so the track prevents nearby text from reflowing.

    Watch the trailing arrow. Without a reserved track, the wght+wdth axis changes alter glyph width every frame, so the row reflows and the arrow oscillates left and right. With width: 4ch reserved, the word renders inside a fixed track and the arrow stays put.

    PitfallWhy does morphing text cause layout shift?

    Axis changes can alter glyph width and line metrics. Put the animated word in a reserved inline track, then animate inside that track instead of letting the line remeasure on every frame.

  4. Line 28font-weight is kept in sync as a fallback and as a readable declaration. font-variation-settings carries the exact multi-axis values.
    PitfallWhich browsers support font-variation-settings?

    Modern Chromium, Firefox, and Safari support CSS variable font settings, but axis availability depends on the actual font. Use @supports for fallback styling and keep the text readable without animation.

  5. Line 32The keyframe changes axis values, not transforms. That preserves glyph drawing quality and avoids scaling text shadows, focus outlines, or hit areas.

    Both words start identical. scaleX(1.8) stretches the existing glyph horizontally and ALSO scales the underline, drop-shadow, and outline — strokes thin out, the underline becomes a fat smear. Animating the wght axis from 300 to 900 makes the font redraw at a heavier weight, so the strokes thicken naturally while the underline and shadow stay sharp.

    PitfallCan I use transform: scaleX() instead of a width axis?

    It is a visual shortcut, not a variable font morph. scaleX distorts the glyph geometry and also scales shadows and hit areas. Use wdth when the font supports it, and fall back to static text when it does not.

  6. Line 42Unsupported browsers get a stable heavy weight. The fallback should be readable static type, not invisible text waiting for unsupported axes.
    PitfallWhich browsers support font-variation-settings?

    Modern Chromium, Firefox, and Safari support CSS variable font settings, but axis availability depends on the actual font. Use @supports for fallback styling and keep the text readable without animation.

  7. Line 45Reduced motion pins a balanced static axis state. It removes the repeated morph while keeping the same word and layout footprint.
    PitfallAre font-variation animations expensive?

    They can trigger text rasterization work, especially at large sizes or on many elements. Keep them short, limit the number of simultaneously morphing words, and avoid combining them with heavy blur filters.

Advanced

Stagger the axis morph per character so the wave ripples through the word

Same font, same wght/wdth/slnt keyframe values, same 3s ease-in-out cycle. BEFORE runs the keyframe on the whole word as one unit — every glyph hits peak weight together and the morph reads as a synchronized pulse. AFTER wraps each character in its own span and applies the SAME keyframe with 60ms stagger per nth-child. The wave of axis change ripples left-to-right through the word — same variable-font technique, just at glyph granularity instead of word granularity.

View explanation and full code67 lines

The base recipe animates font-variation-settings on the whole word as a single block — every glyph hits peak weight + width together. Production variable-font headlines (Stripe, Vercel, GitHub Octicons docs) wrap each character in its own span and stagger the SAME keyframe by ~60ms per glyph. The wave of axis change ripples left-to-right through the word, like the wght/wdth/slnt values are being typed in real time. Pure CSS extension — same font-variation-settings keyframe, same axes; just applied at glyph granularity instead of word granularity, with animation-delay per nth-child.

Paste this as a complete alternative to the base recipe — the word HTML changes from one <span>Axis Morph</span> to one <span> per character, and the animation moves from the wrapper down to each character.

html
<p class="varfont-demo">  <span class="varfont-demo__label">Current mode</span>  <span class="varfont-demo__word" aria-label="Axis Morph">    <span aria-hidden="true">A</span><span aria-hidden="true">x</span><span aria-hidden="true">i</span><span aria-hidden="true">s</span><span aria-hidden="true">&nbsp;</span><span aria-hidden="true">M</span><span aria-hidden="true">o</span><span aria-hidden="true">r</span><span aria-hidden="true">p</span><span aria-hidden="true">h</span>  </span></p> <style>.varfont-demo {  display: grid;  gap: .5rem;  width: min(100%, 18rem);  margin: 0;  color: #f8fafc;  font-family: "Roboto Flex", ui-sans-serif, system-ui, sans-serif;}.varfont-demo__label {  color: #93c5fd;  font: 700 .72rem/1.2 ui-sans-serif, system-ui;  letter-spacing: .08em;  text-transform: uppercase;}.varfont-demo__word {  display: inline-block;  width: min(100%, 14ch);  overflow: hidden;  color: #fff7ed;  font-size: clamp(2rem, 9vw, 4rem);  text-shadow: 0 0 24px rgba(251, 191, 36, .2);}.varfont-demo__word > span {  display: inline-block;  font-weight: 430;  font-variation-settings: "wght" 430, "wdth" 96, "slnt" 0;  animation: varfont-morph-recipe-axis 3s ease-in-out infinite alternate;}.varfont-demo__word > span:nth-child(1)  { animation-delay:  0ms; }.varfont-demo__word > span:nth-child(2)  { animation-delay: 60ms; }.varfont-demo__word > span:nth-child(3)  { animation-delay: 120ms; }.varfont-demo__word > span:nth-child(4)  { animation-delay: 180ms; }.varfont-demo__word > span:nth-child(5)  { animation-delay: 240ms; }.varfont-demo__word > span:nth-child(6)  { animation-delay: 300ms; }.varfont-demo__word > span:nth-child(7)  { animation-delay: 360ms; }.varfont-demo__word > span:nth-child(8)  { animation-delay: 420ms; }.varfont-demo__word > span:nth-child(9)  { animation-delay: 480ms; }.varfont-demo__word > span:nth-child(10) { animation-delay: 540ms; }@keyframes varfont-morph-recipe-axis {  from {    font-weight: 430;    font-variation-settings: "wght" 430, "wdth" 96, "slnt" 0;  }  to {    font-weight: 860;    font-variation-settings: "wght" 860, "wdth" 112, "slnt" -6;  }}@supports not (font-variation-settings: normal) {  .varfont-demo__word > span { font-weight: 760; }}@media (prefers-reduced-motion: reduce) {  .varfont-demo__word > span {    animation: none;    font-weight: 650;    font-variation-settings: "wght" 650, "wdth" 100, "slnt" 0;  }}</style>

Notes

Overview

Variable typography lets a single font file expose continuous axes — wght (weight), wdth (width), slnt (slant). Animating font-variation-settings across these axes morphs the glyph shape live without layout shift (because the track reserves the heaviest / widest frame ahead of time).

When to use it

Reach for variable-font morph on hero headlines, animated logos, and editorial pull-quotes that want type-design personality. Skip it for body copy — the morph reads as noise at reading sizes. Skip it for content that ships without the matching variable font — the morph silently degrades to nothing.

How it works

Set the variable font as a CSS @font-face with font-variation-settings: "wght" 400 as base. Animate via a @keyframes rule that transitions font-variation-settings: "wght" 100 through "wght" 900 and back. The browser interpolates the axis value continuously; the font binary contains every intermediate weight as part of a single file. The track reserves layout for the heaviest weight ahead of time via font-synthesis-weight: none and a generous letter-spacing buffer.

Production gotchas

Browsers still cannot interpolate font-variation-settings by individual axis name — you must list all animated axes in every keyframe, in the same order, or the interpolation breaks and the font snaps. Variable fonts can be 2–3x larger than a static cut; subset aggressively for production. Layout shift between weights is the silent killer: the heaviest weight is wider, so without explicit width reservation the line reflows mid-animation. Use font-variation-settings: "wght" 900 as your reservation baseline.

Accessibility

The morph is purely visual — screen readers see unchanged text. Under prefers-reduced-motion: reduce drop the animation and pin the font to a mid-axis value (e.g. weight 500). Some readers with custom typography (e.g. OpenDyslexic) override the font entirely; the morph silently disappears, which is the correct behavior — do not fight reader customization.

References

Implementation depth

Variable font morphing is strongest when it changes emphasis without changing layout. Animate font-variation-settings across wght or wdth axes while reserving enough inline space for the widest state.

Do not assume every font exposes the same axes. Feature-detect the chosen family, avoid swapping fonts mid-animation, and use reduced motion to pin the most readable weight rather than the most dramatic one.