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.
pnpm dlx shadcn@latest add @caliper/diff-textWhen 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
deland added wordsins; in review mode each run starts with a hidden "removed" or "added" for screen readers.
Installation
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
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.
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.
Related
- Edit reviewReview what an agent changed across several files. Files with changes left to decide stay on top and settle to the bottom as you finish them, each file is a sweep-apply review of its own, and Accept all travels through the files one after another, then offers Undo.
- File diffA line-by-line comparison of two versions of a file.
- Text motionText reveals, morphs and shimmer.