Letters gliding between two words on a laptop screen with a gray-blue British shorthair cat watching in the background.

Calligraph: Where Letters Learn to Slide Instead of Snap

The Gray Cat
The Gray Cat
0 views

When a label on a page flips from "Saving…" to "Saved", what usually happens is unceremonious: the whole word vanishes and a different word pops in to take its place. Your eye registers a flicker, maybe a tiny layout jump, and the moment passes without any sense of continuity. It works, but it feels mechanical. The letters that the two words share — the "S", the "a" — never get to acknowledge that they were there the whole time.

Calligraph is a small React library that fixes exactly this. When the text inside it changes, it figures out which characters the old and new strings have in common, keeps those around, and slides them to their new positions. Characters that are leaving fade out, characters that are arriving fade in, and the result is a fluid morph rather than a hard swap. It is published on npm as calligraph, it is powered by Motion, and the whole thing weighs less than five kilobytes gzipped.

This is the kind of detail you do not notice when it is missing and cannot stop noticing once it is there. It is perfect for status labels, animated counters, prices, segmented control labels, "now playing" text, and anywhere else a string quietly changes while the user is looking.

Why Sliding Beats Swapping

The trick that makes Calligraph feel right is the diffing. Under the hood it computes the longest common subsequence between the previous string and the next one. That subsequence is the set of characters that exist in both, in the same order, and Calligraph gives each of those a stable React key so that React and Motion treat them as the same element across the change. Because they are the same element, Motion's layout animation can move them from where they were to where they need to be.

This is the difference between an animation that delights and one that distracts. If you naively key characters by their index — character zero, character one, and so on — then turning "Hello" into "Help" would make the second "l" appear to teleport, because the element at a given index suddenly holds a different letter. Calligraph sidesteps the whole problem by tracking character identity, not position.

On top of that core behavior, the library layers in a few thoughtful extras: presets for the feel of the spring, a "drift" effect that scatters entering and leaving characters by an amount proportional to how much actually changed, a vertical trend so letters can rise or fall as they enter, and dedicated variants for animating numbers as rolling digits or spinning slot-machine columns. None of it is required. The default behavior is good, and the knobs are there when you want them.

Getting It Into Your Project

Installation is a single package, and Calligraph ships its TypeScript types in the box.

npm install calligraph
yarn add calligraph

One thing to know before you start: Calligraph keeps itself featherweight by declaring Motion, React, and React DOM as peer dependencies rather than bundling them. It expects motion version 11 or newer — that is the modern unified Motion package, the successor to Framer Motion — along with React 18 or newer. If your project already uses Motion, there is nothing extra to add. If it does not, you will want to install it alongside:

npm install motion

Because the library declares sideEffects: false and ships as a clean ES module with a single named export, it tree-shakes perfectly and adds only its own roughly 4.7 KB gzipped to your bundle.

A First Morph

The entire public surface is one component: Calligraph. You give it text as children, and whenever that text changes, it animates. Here is the smallest possible example, where a button swaps one greeting for another.

import { Calligraph } from "calligraph";
import { useState } from "react";

export function Greeting() {
  const [text, setText] = useState("Hello");

  return (
    <>
      <Calligraph>{text}</Calligraph>
      <button onClick={() => setText("World")}>Change</button>
    </>
  );
}

When you click the button, the shared characters glide to their new homes while the rest fade in and out. There is no configuration to learn, no imperative animation API to call, and no refs to wire up. You change the string, Calligraph does the rest. It accepts a string or a number as its children, so you do not need to stringify values yourself.

Calligraph also forwards every standard span prop, which means className, style, id, and aria-* attributes all flow straight through. Styling it is exactly like styling any other inline element.

<Calligraph className="text-lg font-medium text-slate-800">
  {status}
</Calligraph>

Choosing How It Moves

Out of the box the component renders a span, but you can change the wrapper element with the as prop, which is handy when the animated text is actually a heading or a button label. Three named animation presets — smooth, snappy, and bouncy — control the personality of the spring without making you hand-tune stiffness and damping.

<Calligraph as="h1" animation="bouncy" className="text-4xl font-bold">
  {title}
</Calligraph>

The smooth preset is the relaxed default for text; snappy is quicker and is what numbers use by default; bouncy adds a satisfying overshoot. Picking a preset is usually all the tuning you need, but if you want to influence the rhythm of the animation directly, the stagger prop controls how much delay is spread across the characters, measured in seconds. A larger value makes the letters cascade one after another; the default is a tight 0.02.

<Calligraph stagger={0.05}>{headline}</Calligraph>

By default the component also animates its own width so the surrounding layout grows and shrinks smoothly as the text gets longer or shorter, with no jarring reflow. That behavior lives behind the autoSize prop, which is on by default and can be switched off if you would rather manage the width yourself.

Letting Characters Drift and Trend

This is where Calligraph stops being merely functional and starts being expressive. For the default text variant, two props shape how characters enter and leave the stage.

The drift prop sets the maximum spread, in pixels, that arriving and departing characters scatter across. You give it an { x, y } object, and the clever part is that the actual distance is scaled by the fraction of characters that changed. A one-letter edit produces a gentle nudge; rewriting half the string produces a much wider scatter. The motion stays proportional to the meaning of the change.

The trend prop gives that movement a vertical direction: 1 makes characters enter from below, -1 makes them enter from above, and 0 keeps them flat. Combined, the two props let you tune the entrance of new text to feel like it is rising into view or settling down from above.

<Calligraph drift={{ x: 24, y: 8 }} trend={1} animation="snappy">
  {label}
</Calligraph>

If you want something to fire when the dust settles — re-enabling a button, advancing a sequence, logging an event — the onComplete callback runs once the final character has finished animating.

<Calligraph onComplete={() => setReady(true)}>
  {message}
</Calligraph>

There is also an initial prop, off by default, that animates the characters in on the component's very first mount rather than waiting for a change. Turn it on when you want the text to make an entrance as the page loads, and leave it off when you want the initial render to appear instantly and only animate on subsequent updates.

Numbers That Roll and Spin

Text is only half the story. Calligraph ships two variants built specifically for numeric values, and you opt into them through the variant prop.

Set variant="number" and the component switches from character diffing to rolling vertical digits — the kind of effect you have seen on polished dashboards and pricing pages, where a figure smoothly tumbles from one value to the next instead of being replaced.

<Calligraph variant="number" animation="snappy">
  {price}
</Calligraph>

This is ideal for live prices, counters, vote tallies, and stat readouts. As the underlying number changes, each digit column animates independently, so going from 35.99 to 42.50 rolls only the digits that actually moved.

For something with more theatre, variant="slots" renders the digits as spinning slot-machine columns, each one whirling into its final position. It is a flashier effect, well suited to a reveal moment — a final score, a total raised, a countdown landing on zero.

<Calligraph variant="slots">{count}</Calligraph>

Both numeric variants share the same preset and stagger controls as the text variant, so you can dial in how quick or how playful the rolling feels. The drift and trend props are specific to the text variant and are simply ignored for numbers, which keeps the API consistent — you set what makes sense and the component does the right thing.

How It Stacks Up

Calligraph occupies a deliberately narrow lane, and that focus is its strength. It is not a general-purpose animation toolkit like Motion's raw layout prop or react-flip-toolkit, both of which can morph text if you are willing to write the character diffing and keying yourself. Calligraph is that work, already done and packaged into one component. It is also not a one-trick number animator like NumberFlow, though its number and slots variants cover much of the same ground; Calligraph adds full text morphing on top.

Tools in the adjacent "split the text and animate each piece" space, such as SplitType or GSAP's SplitText, solve a genuinely different problem — they break a single string into characters for entrance and scroll effects, but they do not diff an old string against a new one or morph between two values. Calligraph's entire reason for existing is that transition between two states.

The honest caveats: the library is young, having first appeared in early 2026, and it is maintained by a single author. Its published README also trails the real API — the typed component surface, with its variants and drift and trend props, is richer than the basic example the README shows, so trust the TypeScript types as the source of truth. And it does require Motion 11 as a peer dependency, which is a non-issue if you already use Motion and a small addition if you do not. For the niche it serves, those are easy trade-offs.

A Small Detail Worth Having

Calligraph is the sort of library that earns its place in a project not by doing something impossible, but by doing something tedious so well that you stop thinking about it. Character-aware text morphing is finicky to get right by hand, and getting the keying wrong produces exactly the teleporting, flickering letters you were trying to avoid. By leaning on a longest-common-subsequence diff and letting Motion handle the layout animation, Calligraph delivers the effect in a single component with sensible defaults and a handful of expressive knobs.

If your interface has text that changes while the user is watching — and almost every interface does — it is worth a few minutes to wrap that text in <Calligraph> and see how much more intentional the moment feels. The letters that stayed the same finally get to slide into place, and the ones that came and went do so with a little grace. That is a small thing. It is also exactly the kind of small thing that separates an interface that works from one that feels alive.