Text Morph
A smooth morphing transition between two strings — shared characters slide to their new positions while the rest fade out and in, with auto-rotation and controlled switching.
Loading preview…
npx shadcn@latest add https://webberui.com/r/text-morph.jsonPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
3
0.5
10
<TextMorph />
Installation
npx shadcn@latest add https://webberui.com/r/text-morph.jsonOr, once registries are configured in components.json, install it as @webberui/text-morph.
Usage
import { TextMorph } from "@/components/ui/text-morph";
// Auto-rotation (every 3 seconds by default)
<TextMorph
texts={["Build faster.", "Ship sooner."]}
className="text-4xl font-bold"
/>
// controlled mode: external state decides which string is shown
const [index, setIndex] = useState(0);
<TextMorph texts={plans} index={index} interval={0} />Props
| Prop | Type | Default | Description |
|---|---|---|---|
texts | string[] | — | The list of strings to morph through (at least one) |
index | number | — | controlled mode: the index currently shown (automatically taken modulo the length) |
defaultIndex | number | 0 | Initial index in uncontrolled mode |
onIndexChange | (index: number) => void | — | callback fired when the index is about to change (from both auto-rotation and controlled switching) |
interval | number | 3 | Auto-rotation interval (seconds); set to 0 to disable |
duration | number | 0.5 | Duration of a single morph animation (seconds) |
y | number | 10 | Vertical offset for characters entering and leaving (px) |
blur | boolean | true | Whether entering and leaving are accompanied by a blur |
caseSensitive | boolean | false | Whether character matching is case-sensitive; when false, H slides into h |
pauseOnHover | boolean | true | Pause auto-rotation while the mouse hovers |
How it works
- Character matching: each grapheme gets a stable key from "character + nth occurrence". Characters shared by the old and new strings slide smoothly to their new positions via Motion layout animation, and the rest drift up and out while new ones enter from below — producing a "morph" rather than a whole-page swap
- Grapheme splitting:
Intl.Segmentersplits on graphemes, so emoji, flags, and Chinese combining characters are never broken apart - controlled and uncontrolled modes: passing
indexmakes it controlled. Auto-rotation reports the next index throughonIndexChangein both modes, and the controlled side decides whether to accept it - Timer management: the rotation timer is cleaned up correctly on unmount, on hover pause, and on parameter changes, so nothing is left behind
Accessibility
- When the user has "reduce motion" enabled at the system level, the text still rotates but switches instantly with no morph animation
- The animation layer is hidden from assistive technology (
aria-hidden), and the current complete string is exposed separately assr-only, so what is announced is a complete sentence rather than scattered characters pauseOnHoveris on by default so users can stop the rotation to read; if the content matters, pairing it with controlled mode and explicit switching controls is recommended