Components/TextMorph

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.

VerifiedSince 2.4.14

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

Loading demo...

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

Loading demo...

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

Loading demo...

API

TxTextMorph Props

PropTypeDefaultDescription
textstring | number-The value to render. Numbers are formatted with locale + decimals first.
tagstringspanRoot element tag.
durationMsnumber400Morph duration in ms. Ignored when spring is set.
easingstringcubic-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.
scalebooleantrueScale exiting segments as they leave.
numbersbooleantrueRoll numeric words by place value; off falls back to the character morph.
decimalsnumber-Fraction digits. Only applied when text is a number.
localestringenUsed for segmentation and number formatting.
cursorIndexnumber-Caret position. Switches a single-number value from place matching to caret matching, for fields the user types into.
disabledbooleanfalseSkip the animation and write the value straight in.
respectReducedMotionbooleantrueTreat prefers-reduced-motion: reduce as disabled.
debugbooleanfalseOutline the root and every segment, for working out why a morph looks wrong.

TxTextMorph Events

EventPayloadDescription
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-complete and animation-cancel runs per morph.

Behaviour Contract

  • The whole value lives in one visually-hidden but readable [tx-morph-sr] node; every segment element is aria-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: reduce or disabled the value is written straight to textContent and 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 \n in the value, which becomes a <br>; it does not soft-wrap and it does not ellipsise.
  • cursorIndex is 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 cursorIndex for a number the user is typing, or inserting a digit before 20 reads as renumbering the whole column.
  • Use TxTextTransformer with mode="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 TxAutoSizer is 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 own liquid/src/spring.ts.
  • Types: packages/tuffex/packages/components/src/text-morph/src/types.ts exports TextMorphProps.
  • Export alias: packages/tuffex/packages/components/src/text-morph/index.ts exports TextMorph, TxTextMorph, TextMorphProps, TxTextMorphInstance, plus the engine's TextMorphEngine / MorphController.
  • Coverage: engine.test.ts and text-morph.test.ts under packages/tuffex/packages/components/src/text-morph/__tests__/ cover segmentation, diff pairing, place-value matching, the spring fusion and the reduced-motion fallback.
查看源码
packages/tuffex/packages/components/src/text-morph/index.ts