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 calligraph

Calligraph 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

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.

Raphael Salaja

Raphael Salaja

Design Engineer

Leave a Reply

Your email address will not be published. Required fields are marked *