Text Morph

Animates a label from one value to the next, glyph by glyph

Composableoriginal, no Base UI counterpart

TextMorph animates text that changes in place: letters the old and new value share slide to their new spots, and the rest animate out and in. Use it for labels that swap (Copy to Copied, a section title in a header), live numbers such as prices and counters, and status text.

The default mode, blend, is Cubby's own. The two secondary modes are adapted from other libraries: morph from torph and roll from Scritto, rebuilt on the same engine with improvements of their own.

Preview

Design

Installation

Usage

Pass the current text as value and change it; TextMorph works out what moved. The server renders the label already split into glyphs, the same markup the browser keeps, so hydration adds no work and nothing is measured until the value first changes.

Features

  • Numbers by place value. Digits line up on the decimal point, so 1,204 to 1,318 keeps the 1, still and rolls only the digits that changed.
  • Direction. Numbers show which way they went: up when they grow, down when they shrink.
  • Interruptible. A change that lands mid-animation starts every glyph from where it is on screen, so rapid updates never jump.
  • Edge fade. When the box shrinks, old glyphs fade at the moving edge instead of running over the next word or out of a pill.
  • Stable anchoring. Centered and right-aligned labels hold still while the box resizes.
  • Wraps like text. A long value flows over lines, breaking only between words, wherever the surrounding text is allowed to wrap.

Examples

Modes

Set mode to pick how glyphs move: blend (the default), morph or roll. Each button here plays the next change in its mode.

Draft saved
ModeDescription
blendChanges only what changed. Words that stay slide into place, a similar word keeps what it shares at its start and end, and each changed run crossfades in as one unit, slightly smaller and blurred. Digits drift a little the way the number went. Calm enough for anything.
morphMatches whole words, so a word that moves slides over intact, then letters within words that changed. Shared glyphs slide; the rest scale and fade, travelling with the nearest word that stays. Digits roll a full line.
rollKeeps the shared start, end and a shared word in the middle; everything between rolls vertically on a spring.

blend is quick and quiet, a good fit for buttons, tabs, status labels and live values. morph is more expressive, and roll runs about 700ms from the first glyph to the last, which suits headlines and values that change now and then. Set duration to run any mode on a shorter or longer clock: fades, width and stagger scale with it, so the look holds.

Numbers

Pass a number as value and set format (any Intl.NumberFormat options, such as { style: "currency", currency: "EUR" }) and locale to format it; only the digits that change roll. A number is a whole word, so a version (v1.2.3), a date (2024-01-01) or a name with digits (COVID-19) animates as text. Set tabular-nums on numbers, or digits of different widths shift their neighbours on every change.

$1,204.50

In a sentence

Place TextMorph inline with other text; when the value shrinks, the old ink fades at the edge instead of overlapping the words after it.

Deploying to production-eu in the next window.

Wrapping

Give TextMorph a value longer than its line and it wraps between words; glyphs travel to their new places across lines.

Your order has shipped and is on its way to the sorting center.

Editable field

Lay a real input over the number with its text transparent, and the digits someone types are the ones that animate: the input keeps the typing, caret and selection, and TextMorph draws what they see. Pass the input's caret as cursorIndex, so a digit typed mid-number is inserted instead of renumbering the ones after it. blend keeps up with typing as it is; with morph or roll, give it a shorter duration.

Accessibility

Screen readers get the value as plain text; the animated glyphs are hidden from the accessibility tree. Under prefers-reduced-motion: reduce, changes crossfade in place: nothing travels, scales, tilts, blurs or resizes (reducedMotion changes when that applies). TextMorph doesn't announce changes on its own; wrap it in an element with aria-live="polite" when a change needs to be read out, but not around a value that ticks every second, which would talk over everything else.

API Reference

TextMorph renders a <span> through Base UI's useRender, so it accepts a render prop and any native <span> prop in addition to those documented below.

Props

TextMorph

Animates its value from one text to the next, glyph by glyph.

Options

Every option has a default in each mode, exported as MODE_DEFAULTS (read-only). Set only the ones you want to change in options.

Styling

The root carries data-slot="text-morph", data-mode with the current mode, and data-animating while a change plays, so data-animating: variants style a label mid-change. Two CSS variables tune each glyph's slot, the room that fades a rolling glyph out above and below its line:

VariableDescription
--text-morph-slot-yHow far a glyph's slot reaches above and below its line. 0.3em; less in morph.
--text-morph-fadeHow far into the slot the fade runs. 0.3em; 0.15em in morph.

Other data-* attributes inside the label are internal and may change. The component imports text-morph.css itself; a setup that doesn't allow global CSS imported from a component (the Next.js Pages Router) needs it imported once where global styles go.

Notes

  • Off screen. A change to a label that isn't on screen at that moment, or in a background tab, swaps instantly.
  • Width. A change that stays on one line eases the box to its new width, so text after an inline TextMorph slides instead of jumping. A change that wraps keeps the lines as they fall, without the width animation or the edge fade.
  • Height. Inline, the label's height is its lines', so gaining or losing a line changes it at once. Give it className="block" (it's a box already inside a flex or grid container) and a change of line count eases its height too, so the card or paragraph around it grows and shrinks smoothly. For inline-block, add align-top: aligned on its last line's baseline, an inline-block stretches its line to the new height at once as it grows.
  • Languages. Right-to-left text works, and a label reads in its own direction inside text of the other. A word in a script whose letters join or stack (Arabic, Persian, Urdu, Hindi and the other Indic scripts, Thai, Khmer and others) animates as a whole word so it stays correctly shaped; other text animates letter by letter. Numbers in any script's digits match by place value.
  • Spaces. Spaces show as typed: a run of them isn't collapsed to one, and a \n in the value starts a new line.
  • Empty values. An empty value keeps its line's height, so the text around it doesn't jump.
  • Keep render on one element type. Switching it to a different element or component while mounted remounts the label with its first value's glyphs.
  • Built for labels and numbers. Each glyph keeps its own compositing layer so it doesn't shift as an animation ends, and each layer takes a little GPU memory. A few labels on a page cost nothing noticeable; hundreds of changing values or long passages add up, most on low-end phones.
  • No spaces, no wrapping. A value without spaces (a long number, a URL) can't break, so it runs past a narrow container, like any unbroken word.