---
title: "TextMorph"
description: "Text that animates only the segments that changed."
category: Effects
status: beta
since: 0.6.0
tags: [text, morph, number, spring, live-region]
syncStatus: reviewed
verified: true
---

## Usage

### Basic
Only the segments that actually changed move, and the container's size follows on the same curve.
:::TuffDemoWrapper{demo="TextMorphTextMorphDemo" code-lang="vue"}
---
code: |
  <script setup lang="ts">
  import { computed, ref } from 'vue'

  const phrases = [
    'Hello world',
    'Hello there',
    'Goodbye there',
    'Goodbye and thanks for all the fish',
  ]

  const index = ref(0)
  const phrase = computed(() => phrases[index.value] ?? '')

  function next() {
    index.value = (index.value + 1) % phrases.length
  }
  </script>

  <template>
    <TxButton @click="next">Next phrase</TxButton>
    <TxTextMorph :text="phrase" :duration-ms="400" />
  </template>
---
:::

### Number Place Value
Digits match by place value, not left to right: `1,204 → 1,318` rolls the hundreds and tens and leaves the thousands alone.
:::TuffDemoWrapper{demo="TextMorphNumberPlaceValueDemo" code-lang="vue"}
---
code: |
  <script setup lang="ts">
  import { ref } from 'vue'

  const total = ref(1204)
  </script>

  <template>
    <!-- 1,204 -> 1,318 rolls the hundreds and tens; the thousands stay put -->
    <TxTextMorph :text="`$${total.toLocaleString('en')}`" />

    <!-- Pass a number directly and locale + decimals do the formatting -->
    <TxTextMorph :text="total" locale="en" :decimals="2" />
  </template>
---
:::

### Springs
`spring` takes a preset name or coefficients and sets both curve and duration, so `durationMs` and `easing` are ignored.
:::TuffDemoWrapper{demo="TextMorphSpringDemo" code-lang="vue"}
---
code: |
  <template>
    <TxTextMorph text="Settling" spring="snappy" />
    <TxTextMorph text="Settling" spring="smooth" />
    <TxTextMorph text="Settling" spring="bouncy" />
    <TxTextMorph text="Settling" :spring="{ stiffness: 200, damping: 20 }" />
  </template>
---
:::

### Best Practices

- Reach for it where the same quantity changes: counters, totals, percentages.
- Pass `cursorIndex` for a number the user is typing, or inserting a digit before `20` reads as renumbering the column.
- Use `TxTextTransformer` with `mode="fade"` where text must soft-wrap or ellipsise; here only `\n` breaks a line.
- Keep high-frequency instances on one screen few; every character is an element with `will-change`.
- Don't wrap it in `TxAutoSizer`; the engine animates the container's width and height itself.

## API Reference

### Props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `text` | `string \| number` | - | The value to render; numbers are formatted with `locale` and `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; sets both the curve and the duration. |
| `scale` | `boolean` | `true` | Scales exiting segments as they leave. |
| `numbers` | `boolean` | `true` | Rolls numbers by place value; off falls back to the character morph. |
| `decimals` | `number` | - | Fraction digits; applies only when `text` is a number. |
| `locale` | `string` | `en` | Used for segmentation and number formatting. |
| `cursorIndex` | `number` | - | Caret position; switches a single-number value to caret matching. |
| `disabled` | `boolean` | `false` | Skips the animation and writes the value straight in. |
| `respectReducedMotion` | `boolean` | `true` | Treats reduced motion as `disabled`. |
| `debug` | `boolean` | `false` | Outlines the root and every segment, for debugging a morph. |

### Events

| Event | Payload | Description |
|------|------|-------------|
| `animation-start` | - | A morph began; never fires for the first render. |
| `animation-complete` | - | The morph ran to its end; exactly one of this and `animation-cancel` fires per morph. |
| `animation-cancel` | - | The next update interrupted the morph. |

## Overview

- The unit of motion is the character segment, not the string: survivors FLIP to their new position, arrivals fade in from the nearest anchor, and exits leave the flow and fade out.
- The whole value lives in one visually hidden `[tx-morph-sr]` node and every segment is `aria-hidden`, so a screen reader announces the value once.
- Selectable segments keep the original whitespace (NBSP, repeated spaces, tabs, line breaks, combining characters); exiting segments are not selectable, so a copy never mixes in the previous text.
- Grouping commas travel with the magnitude; once the magnitude jumps by three places or more, no digit carries across and the number is replaced.
- An update mid-morph carries the current velocity into the new curve, so rapid updates don't stall at the start of each curve.
- Under reduced motion or `disabled`, the value is written straight to `textContent` and the internal segment record is cleared.

## Technologies

- The engine creates and owns the segment elements imperatively (the first-paint text exists only for SSR hydration), so the styles aren't scoped; `tx-morph-*` attribute names contain them.
- The engine (also exported as `TextMorphEngine` / `MorphController`) is ported from [lochie/torph](https://github.com/lochie/torph) (MIT), with its spring layer replaced by `liquid/src/spring.ts`, shared with `TxLiquid`.
- Source: `packages/tuffex/packages/components/src/text-morph/`.

<TuffDocSourceLink />
