File diff
FreeA line-by-line comparison of two versions of a file.
pnpm dlx shadcn@latest add https://caliperui.site/r/file-diff.json+7 −5
| import { useState } from "react"; | |||
| Removed: export function Counter({ start = 0 }: { start?: number }) { | |||
| Added: export function Counter({ start = 0, step = 1 }: { start?: number; step?: number }) { | |||
| const [count, setCount] = useState(start); | |||
| Removed: const label = "Count: " + count; | |||
| Added: const label = `Count: ${count}`; | |||
| function increment() { | |||
| Removed: setCount(count + 1); | |||
| Added: setCount((current) => Math.min(current + step, maxCount)); | |||
| } | |||
| return ( | |||
| <div className="counter"> | |||
| Removed: <p>{label}</p> | |||
| Removed: <button onClick={increment}>Add</button> | |||
| Added: <p aria-live="polite">{label}</p> | |||
| Added: <button onClick={increment} disabled={count >= maxCount}> | |||
| Added: Add | |||
| Added: </button> | |||
| </div> | |||
| ); | |||
| } | |||
When to use #
Use it for
- Showing what an agent or a commit changed in a file.
- Reviewing edits before they are applied.
Skip it when
- The comparison is of rich text or images.
- Files are very large; page or collapse them first.
Installation #
terminal
pnpm dlx shadcn@latest add https://caliperui.site/r/file-diff.json
Adds its 5 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 #
example.tsx
import { useState } from "react" import { Button } from "@/components/ui/button" import { FileDiff } from "@/components/ui/file-diff" const before = `import { useState } from "react"; export function Counter({ start = 0 }: { start?: number }) { const [count, setCount] = useState(start); const label = "Count: " + count; function increment() { setCount(count + 1); } return ( <div className="counter"> <p>{label}</p> <button onClick={increment}>Add</button> </div> ); } export const defaultStart = 0; export const maxCount = 99; `; const after = `import { useState } from "react"; export function Counter({ start = 0, step = 1 }: { start?: number; step?: number }) { const [count, setCount] = useState(start); const label = \`Count: \${count}\`; function increment() { setCount((current) => Math.min(current + step, maxCount)); } return ( <div className="counter"> <p aria-live="polite">{label}</p> <button onClick={increment} disabled={count >= maxCount}> Add </button> </div> ); } export const defaultStart = 0; export const maxCount = 99; `; export function FileDiffDemo() { const [, setDecision] = useState(""); return ( <FileDiff path="src/counter.tsx" before={before} after={after} actions={ <> <Button variant="secondary" size="sm" onClick={() => setDecision("Rejected src/counter.tsx")}>Reject</Button> <Button size="sm" onClick={() => setDecision("Accepted src/counter.tsx")}>Accept</Button> </> } /> ); }
API reference #
FileDiff
component
PropTypeDefaultDescription
path *string—
before *string—
after *string—
languageCodeLanguage—
contextnumber3Unchanged lines kept around each change.
view"unified" | "split""unified"
actionsReactNode—Buttons for the header, such as Accept and Reject.
defaultOpenbooleantrue
classNamestring—
diffLines
component
No props of its own; it accepts the native props of its root element.
Accessibility #
It exposes group and region semantics, with aria-label, aria-expanded, aria-controls, aria-hidden and aria-pressed kept in step with state.
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.