TextMorph
Cuts a string into segments with identity, diffs it, and animates only what actually changed; numbers roll by place value, the container transitions on the same curve, and springs and mid-flight interruption are supported.
TextMorph
Unlike
TxTextTransformer's whole-string crossfade, the unit of motion here is the character segment. Surviving segments FLIP to their new position, arriving ones fade in from the nearest anchor, exiting ones leave the flow and fade out, and numbers additionally slide along the block axis by place value.
Basic Usage
TextMorph
Numeric Place-Value Roll
Digits are matched by place value rather than left to right: 1,204 → 1,318 rolls the hundreds and the tens and leaves the thousands alone, and the grouping comma travels with the magnitude, sliding a whole group along on 999,999 → 1,000,000.
Once the magnitude jumps by three places or more nothing carries across — at that point the digits overlap into a smear and a replacement is the honest reading.
Turn numbers off to fall back to the character-level morph.
Number place value
Spring Easing
spring takes a preset name or physical coefficients, and runs on the same spring compiler TxLiquid and TxSlider use, so the whole library shares one motion vocabulary. A spring supplies both the curve and the duration, so durationMs and easing are ignored while one is set.
Spring presets
API
TxTextMorph Props
| Prop | Type | Default | Description |
|---|---|---|---|
text | string | number | - | The value to render. Numbers are formatted with locale + decimals first. |
tag | string | span | Root element tag. |
durationMs | number | 400 | Morph duration in ms. Ignored when spring is set. |
easing | string | cubic-bezier(0.19, 1, 0.22, 1) | CSS timing function. Ignored when spring is set. |
spring | 'snappy' | 'smooth' | 'bouncy' | { stiffness?, damping?, mass? } | - | Spring physics. Supplies both the curve and the duration. |
scale | boolean | true | Scale exiting segments as they leave. |
numbers | boolean | true | Roll numeric words by place value; off falls back to the character morph. |
decimals | number | - | Fraction digits. Only applied when text is a number. |
locale | string | en | Used for segmentation and number formatting. |
cursorIndex | number | - | Caret position. Switches a single-number value from place matching to caret matching, for fields the user types into. |
disabled | boolean | false | Skip the animation and write the value straight in. |
respectReducedMotion | boolean | true | Treat prefers-reduced-motion: reduce as disabled. |
debug | boolean | false | Outline the root and every segment, for working out why a morph looks wrong. |
TxTextMorph Events
| Event | Payload | Description |
|---|---|---|
animation-start | - | A morph began. Never fires for the first render. |
animation-complete | - | The morph ran to its end. |
animation-cancel | - | The morph was interrupted by the next update. |
Exactly one of
animation-completeandanimation-cancelruns per morph.
Behaviour Contract
- The whole value lives in one visually-hidden but readable
[tx-morph-sr]node; every segment element isaria-hidden, so a screen reader announces the value once and never the fragments. - Segments are created imperatively by the engine, so the component's styles are not scoped — containment comes from the
tx-morph-*attribute names instead. - Vue stops rendering the children after mount: the engine owns them. The first-paint plain text exists only so hydration matches.
- Under
prefers-reduced-motion: reduceordisabledthe value is written straight totextContentand the internal segment record is cleared, so re-enabling motion never FLIPs against elements that have already left the DOM. - An update mid-morph reads the velocity the box is travelling at and carries it into the new curve, so a fast run of updates does not leave every curve stalled in its opening sliver.
- The root is
white-space: nowrap. Line breaks come from\nin the value, which becomes a<br>; it does not soft-wrap and it does not ellipsise. cursorIndexis honoured only when the value holds exactly one number.
Best Practices
- Reach for it where the same quantity is changing — counters, totals, percentages. Place-value matching was written for exactly that.
- Pass
cursorIndexfor a number the user is typing, or inserting a digit before20reads as renumbering the whole column. - Use
TxTextTransformerwithmode="fade"where the text has to soft-wrap or ellipsise; the engine cannot express either. - Keep the number of high-frequency instances on one screen down: every character is an element carrying
will-change. - Let it size its own container — the engine animates width and height itself, so wrapping it in
TxAutoSizeris redundant.
Source
- Component source:
packages/tuffex/packages/components/src/text-morph/src/TxTextMorph.vue. - Engine:
packages/tuffex/packages/components/src/text-morph/src/engine/, ported from lochie/torph (MIT), with the spring layer replaced by TuffEx's ownliquid/src/spring.ts. - Types:
packages/tuffex/packages/components/src/text-morph/src/types.tsexportsTextMorphProps. - Export alias:
packages/tuffex/packages/components/src/text-morph/index.tsexportsTextMorph,TxTextMorph,TextMorphProps,TxTextMorphInstance, plus the engine'sTextMorphEngine/MorphController. - Coverage:
engine.test.tsandtext-morph.test.tsunderpackages/tuffex/packages/components/src/text-morph/__tests__/cover segmentation, diff pairing, place-value matching, the spring fusion and the reduced-motion fallback.