The Future of Web Dev
The Future of Web Dev
Calligraph: React Text Transitions with Motion
Add character-level text transitions to React with Calligraph, Motion-based movement, fades, and custom spring settings.

Calligraph is a React text transition component that animates changing text with character-level movement, fade effects, and Motion-based transitions.
It works well for UI copy that changes in place, such as pricing labels, status messages, rotating headlines, counters, and compact dashboard text.
Shared characters move to their new positions, new characters fade in, and removed characters fade out.
Features
- Animates text changes at the character level.
- Moves shared characters into their new positions.
- Fades entering characters into the line.
- Fades removed characters out of the line.
- Uses Motion for transition control.
- Accepts custom spring transition settings.
- Works with React 18+ projects.
- Requires Motion 11+.
See It In Action
Use Cases
- Pricing cards need animated plan labels, discount text, or billing-cycle values after a toggle.
- Landing-page headlines feel more polished when rotating phrases keep shared letters in motion.
- Dashboard metrics that mix letters and numbers gain a calmer update pattern than full text replacement.
- AI chat interfaces often change status text from “Thinking” to “Writing” to “Done.”
- Settings screens use short state labels such as “Saving,” “Saved,” and “Failed” inside buttons or inline notices.
How To Use It
Install
Install the package from npm:
npm install calligraphCalligraph targets React 18+ and Motion 11+. Check those versions before adding it to an older React project. ([GitHub][1])
Basic Usage
import { useState } from "react";
import { Calligraph } from "calligraph";
export function ReviewStatus() {
const [status, setStatus] = useState("Draft");
return (
<div>
<Calligraph>{status}</Calligraph>
<button onClick={() => setStatus("Ready")}>
Mark as ready
</button>
</div>
);
}The component watches its text children. When the value changes, matching characters move into place while added and removed characters fade. ([GitHub][1])
Practical Examples
Animated Pricing Toggle
import { useState } from "react";
import { Calligraph } from "calligraph";
export function BillingLabel() {
const [annual, setAnnual] = useState(false);
return (
<section>
<p>
<Calligraph>
{annual ? "$199 per year" : "$19 per month"}
</Calligraph>
</p>
<button onClick={() => setAnnual((value) => !value)}>
Switch billing
</button>
</section>
);
}This pattern fits pricing cards because the text stays in the same layout slot. The animation draws attention to the changed value while the card structure remains stable.
Rotating Product Headline
import { useEffect, useState } from "react";
import { Calligraph } from "calligraph";
const phrases = [
"Build faster reports",
"Review cleaner data",
"Ship better dashboards",
];
export function RotatingHeadline() {
const [index, setIndex] = useState(0);
useEffect(() => {
const timer = window.setInterval(() => {
setIndex((current) => (current + 1) % phrases.length);
}, 2400);
return () => window.clearInterval(timer);
}, []);
return (
<h1>
<Calligraph>{phrases[index]}</Calligraph>
</h1>
);
}Keep rotating phrases close in length when the heading sits inside a narrow hero layout. This reduces layout movement around the animated text.
Custom Spring Transition
import { useState } from "react";
import { Calligraph } from "calligraph";
export function SaveButtonLabel() {
const [saved, setSaved] = useState(false);
return (
<button onClick={() => setSaved(true)}>
<Calligraph
transition={{
type: "spring",
stiffness: 200,
damping: 20,
}}
>
{saved ? "Saved" : "Save changes"}
</Calligraph>
</button>
);
}Use the transition prop when the default motion feels too loose or too sharp for your interface. The example above uses a spring with custom stiffness and damping. ([GitHub][1])
Next.js App Router Usage
Place interactive Calligraph examples inside a Client Component. State updates, click handlers, intervals, and browser timers belong on the client side in the App Router.
"use client";
import { useState } from "react";
import { Calligraph } from "calligraph";
export function ClientStatusText() {
const [state, setState] = useState("Queued");
return (
<div>
<Calligraph>{state}</Calligraph>
<button onClick={() => setState("Running")}>
Start job
</button>
</div>
);
}A Server Component may render static surrounding layout, but the changing text component should live below a client boundary when it depends on user interaction or browser state.
Alternatives and Related Resources
- Smart Ticker: Text Animation for React and Vue
- React Typing Animation Library with WAAPI Blur: Kasumi
- Swap Letters In 2 Words: React Anagram Animation
FAQs
Q: Why does the text not update in my Next.js App Router page?
A: Move the interactive component into a file with "use client". State changes, click handlers, and timers need a Client Component.
Q: How do I change the animation feel?
A: Pass a transition object to Calligraph. Spring settings such as stiffness and damping adjust how quickly characters move into place.
Q: Is Calligraph the same as a typewriter component?
A: No. A typewriter component reveals text character by character. Calligraph animates changes between old and new text values.





