MorphText

Text that transitions between states instead of jumping — a button label that changes partway through a flow, a chip's state word. MorphText (packages/ui/src/components/MorphText.tsx) is a single inline element whose width is carried by the spring (spring, lib/spring.ts — same physics as the dialog window and the list cursor) while the old and new text cross-fade in place. The element that carries the text — a Button, a chip — stays the same element throughout; only its content changes.

Story (packages/ui/stories/morph-text.stories.tsx, States): a primary button's label morphs between "Vybrat záznam" (Select record) and "Založit osobu" (Create person) on click. Live use: the New deal search step's footer primary action (multi-step-dialog, DealSubjectStep) — the button reads Select record while the cursor is on a record and morphs to Create person the moment it moves onto the create row, with no other change to the button.

When to use

  • A label on a persistent control (a button, a chip) changes meaning as the user moves through a flow, and the width difference between the two labels would otherwise cause a visible jump — e.g. multi-step-dialog's footer action, "Select record" while the cursor is on a record, "Create person" while it is on the create row.
  • Any short run of text where "this text changed" should read as one object updating, not as new text appearing next to old text.

When not to use

  • Long or multi-line text, or text whose line count can change — the component measures and animates a single inline box's width; it is not built for reflowing paragraphs.
  • A value that changes on every keystroke or every render (a live counter, typed input) — animate only deliberate state changes, or the spring restarts constantly and never settles (see spring's Best practices).
  • Text that must be read as a value change by assistive tech in real time — MorphText marks the outgoing layer aria-hidden and adds no live region (see morph-text.a11y.md); pair it with an aria-live region on the parent when the change itself must be announced immediately.

Anatomy

  • One prop: text: string. MorphText derives everything else from the transition between the previous and next value.
  • span.acrm-morph-text — the persistent host; overflow: hidden, white-space: nowrap, sizes to its content.
  • span.acrm-morph-text-l — one text layer. Two can be mounted in the same commit: the outgoing layer (.is-leaving) is taken out of flow (position: absolute) and fades out in place over 140ms (acrmMorphTextOut, dialog-morph.css) before it unmounts; the incoming layer (.is-entering) fades in (acrmMorphTextIn, 0.2s, 0.06s delay). Only the incoming layer determines the host's width.
  • Width itself is not CSS-animated — a useLayoutEffect keyed on text reads the host's boundingClientRect before and after the change and calls springAnimate(el, { width: [from, to, 'px'] }).
  • Font and colour are inherited from the parent (a Button, a chip) — the component draws no typography of its own.

Best practices

Do

  • Let the host element (button, chip) keep its own hit target and padding; MorphText only occupies the text slot inside it (see the Default/States stories: <Button><MorphText text=…/></Button>).
  • Change text only on a deliberate state transition (a step change, a picked/typed branch), one value at a time.

Don't

  • Don't wrap MorphText around text that can span multiple lines or wrap — the width spring assumes a single inline box.
  • Don't add a second animation (slide, scale) on top of it — the cross-fade and width spring are the whole motion; anything else would fight the "one object reshaping" read the pattern exists for (RULES.md §11).
  • Don't drive text from something that updates continuously — interrupt-and-restart is handled (a change mid-flight continues from the currently visible width), but a spring that never settles reads as jitter, not motion.
  • spring — the width motion.
  • multi-step-dialog — first live use, in the search step's footer primary action.
  • button — the control MorphText most commonly sits inside.

Source: docs/dialog/morph-text.usage.md · GitLab