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 —
MorphTextmarks the outgoing layeraria-hiddenand adds no live region (seemorph-text.a11y.md); pair it with anaria-liveregion on the parent when the change itself must be announced immediately.
Anatomy
- One prop:
text: string.MorphTextderives 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
useLayoutEffectkeyed ontextreads the host'sboundingClientRectbefore and after the change and callsspringAnimate(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;
MorphTextonly occupies the text slot inside it (see theDefault/Statesstories:<Button><MorphText text=…/></Button>). - Change
textonly on a deliberate state transition (a step change, a picked/typed branch), one value at a time.
Don't
- Don't wrap
MorphTextaround 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
textfrom 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.
Related
spring— the width motion.multi-step-dialog— first live use, in the search step's footer primary action.button— the controlMorphTextmost commonly sits inside.
Source: docs/dialog/morph-text.usage.md · GitLab