torph
| Install | |
|---|---|
npm install torph |
|
| Latest Version: | 0.1.3 |
| Vue: | 3 |
| License: | MIT |
| Last Updated: | Sep 7, 2026 |
| Links: | GitHub · npm |
Torph
Dependency-free animated text morphing component for React, Vue, Svelte, and vanilla JavaScript.
Installation
npm install torph
# or
pnpm add torph
# or
yarn add torph
Framework Usage
React
import { TextMorph } from "torph/react";
function App() {
const [text, setText] = useState("Hello World");
return (
<TextMorph
duration={400}
ease="cubic-bezier(0.19, 1, 0.22, 1)"
locale="en"
onAnimationComplete={() => console.log("Animation done!")}
className="my-text"
as="h1"
>
{text}
</TextMorph>
);
}
React Hook
import { useTextMorph } from "torph/react";
function CustomComponent() {
const { ref, update } = useTextMorph({
duration: 400,
ease: "cubic-bezier(0.19, 1, 0.22, 1)",
});
useEffect(() => {
update("Hello World");
}, []);
return <div ref={ref} />;
}
Vue
<script setup>
import { ref } from "vue";
import { TextMorph } from "torph/vue";
const text = ref("Hello World");
const handleComplete = () => {
console.log("Animation done!");
};
</script>
<template>
<TextMorph
:text="text"
:duration="400"
ease="cubic-bezier(0.19, 1, 0.22, 1)"
locale="en"
:onAnimationComplete="handleComplete"
class="my-text"
as="h1"
/>
</template>
Svelte
<script>
import { TextMorph } from 'torph/svelte';
let text = $state('Hello World');
const handleComplete = () => {
console.log('Animation done!');
};
</script>
<TextMorph
{text}
duration={400}
ease="cubic-bezier(0.19, 1, 0.22, 1)"
locale="en"
onAnimationComplete={handleComplete}
class="my-text"
as="h1"
/>
Vanilla JS
import { TextMorph } from "torph";
const morph = new TextMorph({
element: document.getElementById("morph"),
duration: 400,
ease: "cubic-bezier(0.19, 1, 0.22, 1)",
locale: "en",
onAnimationStart: () => console.log("Starting..."),
onAnimationComplete: () => console.log("Done!"),
});
morph.update("Hello World");
Spring Animations
Pass spring parameters to ease for physics-based easing. The duration is computed automatically from the spring physics.
import { TextMorph } from "torph/react";
function App() {
const [text, setText] = useState("Hello World");
return (
<TextMorph ease={{ stiffness: 200, damping: 20 }}>{text}</TextMorph>
);
}
Spring Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
stiffness |
number |
100 |
Spring stiffness coefficient |
damping |
number |
10 |
Damping coefficient |
mass |
number |
1 |
Mass of the spring |
precision |
number |
0.001 |
Threshold for determining settled position |
Numbers
Numeric words morph by place value: digits slide along the block axis, and the
symbols around them — currency, separators, signs, suffixes — travel with the
places they belong to. It is on by default, so any value that contains a number
already animates this way. Pass numbers={false} to fall back to the
character-level text morph.
import { TextMorph } from "torph/react";
// 1,204 → 1,318 rolls the hundreds and tens, leaves the thousands alone
<TextMorph>{`$${total.toLocaleString("en")}`}</TextMorph>;
A value passed as a number rather than a string is formatted for you, so
locale and decimals apply:
<TextMorph decimals={2} locale="de-DE">
{1234.5}
</TextMorph>
Editable fields
Place matching is the right default for a value that changes on its own — a
counter, a total, a chart readout. It is the wrong one for a field somebody is
typing in, where the character that just changed is known and place value is not
the point: typing 1 in front of 20 should insert a digit, not renumber the
column.
Pass cursorIndex to switch that update from place matching to caret matching.
It is available on the React component and as the second argument to
update(); a caret position is a DOM concern, so there is no Vue or Svelte prop
for it.
const [value, setValue] = useState("");
const [caret, setCaret] = useState<number>();
<input
value={value}
onChange={(e) => {
setCaret(e.target.selectionStart ?? undefined);
setValue(e.target.value);
}}
/>
<TextMorph cursorIndex={caret}>{value}</TextMorph>
morph.update("$120", 2);
API
Options
All components accept the following props/options:
text/children: string- The text to display (required)duration?: number- Animation duration in milliseconds (default:400). Ignored wheneaseis a spring, which settles on its own physicsease?: string | SpringParams- CSS easing function or spring parameters (default:"cubic-bezier(0.19, 1, 0.22, 1)")scale?: boolean- Enable scale animation on exiting segments (default:true)numbers?: boolean- Morph numeric words by place value, sliding digits along the block axis. Off falls back to the character-level text morph (default:true)decimals?: number- Fraction digits to format a numeric value to. Applies when the value is a number (React children, orupdate(number)); ignored for stringslocale?: Intl.LocalesArgument- Locale for text segmentation, and for formatting a numeric value (default:"en")debug?: boolean- Enable debug mode with visual indicatorsdisabled?: boolean- Disable all morphing animations (default:false)respectReducedMotion?: boolean- Respect user's prefers-reduced-motion setting (default:true)onAnimationStart?: () => void- Callback fired when animation beginsonAnimationComplete?: () => void- Callback fired when animation completesonAnimationCancel?: () => void- Callback fired when a morph is interrupted by the next one. Exactly one ofonAnimationCompleteandonAnimationCancelruns per morphclassName?: string- CSS class name (React/Vue:class)style?: object | string- Inline stylesas?: string- HTML element type (default:"span")
Multi-line text
Newlines in a value are rendered as line breaks, and text morphs normally across them. The React component includes them in its server-rendered markup; Vue and Svelte render them once the component mounts.
Utilities
The text matching torph runs on is exported for building your own behaviour on top of it:
segmentText(value, locale): Segment[]- Split a value into segmentsdiffSegments(oldSegments, newText, locale): DiffResult- Match a new value against existing segments, returning the segments that persist, enter and exit
Found this useful?
Follow me on Twitter.
Other projects
You might also like:
- number-flow - Animated number component by Maxwell Barvian.
- easing.dev - Easily create custom easing graphs.
Acknowledgements
- Thanks to Alex for assistance with the site design.
- Thanks to Pugson for putting up with my bullshit.
- Thanks to Benji for coining the
Torphname and outlining the method in Family Values.
License
MIT
Related Packages
Four motions, no dials. rise, leave, morph and reveal on the Web Animations API,...
Live controls for tuning interfaces in React, Solid, Svelte, Vue, and plain Java...
LoaderKit loading and progress indicators for the web: a canvas renderer, and cu...
Version History
| Version | Released | Vue | License |
|---|---|---|---|
| 0.1.3 | >=3 | MIT | |
| 0.1.2 | >=3 | MIT | |
| 0.1.1 | >=3 | MIT | |
| 0.1.0 | >=3 | MIT | |
| 0.0.10 | >=3 | MIT | |
| 0.0.9 | >=3 | MIT | |
| 0.0.8 | >=3 | MIT | |
| 0.0.7 | >=3 | MIT | |
| 0.0.6 | >=3 | MIT | |
| 0.0.5 | >=3 | MIT | |
| 0.0.4 | >=3 | MIT | |
| 0.0.3 | >=3 | MIT | |
| 0.0.2 | >=3 | MIT | |
| 0.0.1 | MIT |