Skip to content
caliper ui docs

Components

Diff text

Shows a change to a piece of text where it stands: removed words are struck through and close up, added words grow in with a passing tint, and then it settles. With `keep`, the marks stay for review.

Free
pnpm dlx shadcn@latest add @caliper/diff-text

Reply draft

Each rewrite plays out in place, then settles.

Thanks for reaching out about the late invoice. We have looked into it and the payment failed because the card on file expired last month.

Review mode keeps the marks

Thanks for removed: reaching out aboutadded: flagging the late invoice. removed: We have looked into it and theadded: The payment failed because the card on file expired last month.

Installs components/ui/diff-text.tsxDepends on none beyond ReactMotion none in component source

When to use

Use it for

  • AI rewrites and suggested edits inside a sentence or paragraph.
  • Changelog lines and settings summaries that update in place.

Skip it when

  • Code or multi-line changes; use `FileDiff`.
  • Changes people did not cause; motion there is distracting.

Decisions

Why it is built this way, so you know what to keep when you change your copy.

Behaviour

Word by word
The comparison is by word with spaces as their own tokens, so a change never eats its neighbours and the text still wraps naturally.
Strike, close, open
Removals strike first, then close up; additions open after, so the eye sees what went before what came.

Usage

Readable marks
Removed words are del and added words ins; in review mode each run starts with a hidden "removed" or "added" for screen readers.

Installation

terminal
pnpm dlx shadcn@latest add @caliper/diff-text

Adds its 3 files to components/ui/, each with its own stylesheet, and installs their packages. No account needed. The first part also adds Caliper UI’s tokens; import them once as in Installation.

Usage

This is the code behind the preview above. It is built from DiffText and diffWords.

example.tsx
import { useState, type ReactNode } from "react"
import { Button } from "@/components/ui/button"
import { DiffText } from "@/components/ui/diff-text"

function Panel({ title, context, className = "max-w-md", children }: { title: string; context: string; className?: string; children: ReactNode }) {
  return (
    <section className={`grid w-full gap-5 rounded-overlay border border-border bg-background p-6 shadow-resting max-sm:p-4 ${className}`}>
      <div className="grid gap-1">
        <p className="font-medium">{title}</p>
        <p className="text-sm text-muted-foreground">{context}</p>
      </div>
      {children}
    </section>
  );
}

const drafts = [
  "Thanks for reaching out about the late invoice. We have looked into it and the payment failed because the card on file expired last month.",
  "Thanks for flagging the late invoice. The payment failed because the card on file expired last month.",
  "Thanks for flagging the invoice: the card on file expired, so the payment failed.",
];

export function DiffTextDemo() {
  const [step, setStep] = useState(0);
  return (
    <Panel title="Reply draft" context="Each rewrite plays out in place, then settles." className="max-w-lg">
      <p className="leading-relaxed">
        <DiffText value={drafts[step]} />
      </p>
      <div className="flex flex-wrap gap-2">
        <Button size="sm" onClick={() => setStep((step + 1) % drafts.length)}>
          {step === drafts.length - 1 ? "Back to the original" : "Make it shorter"}
        </Button>
      </div>
      <div className="grid gap-1.5 border-t border-border pt-4">
        <p className="text-xs text-muted-foreground">Review mode keeps the marks</p>
        <p className="text-sm leading-relaxed">
          <DiffText keep previous={drafts[0]} value={drafts[1]} />
        </p>
      </div>
    </Panel>
  );
}

API reference

DiffText

component

Also accepts the native props of its root element and a ref.

PropTypeDefaultDescription
value *string—The text to show. When it changes, the change plays out in place.
keepbooleanfalseLeaves removed words struck through and added words marked, for review, instead of settling to plain text.
previousstring—The text to compare with when `keep` is on and nothing has changed yet.

diffWords

component

No props of its own; it accepts the native props of its root element.

Accessibility

It is built on native elements, so roles and focus come from the browser.

No custom key handling; Tab and the native activation keys do the work.

Motion

It declares no animation in its component source. Any transition comes from its stylesheet, which uses the duration tokens that collapse to instant under prefers-reduced-motion.

All Text motion examples