Diff row
One line of a diff with old and new line numbers and a sign column.
npx shadcn@latest add https://ui.goodtabs.co/r/diff-row.json
With the @goodtabs namespace in components.json: npx shadcn@latest add @goodtabs/diff-row
const size = 16;Added:
const gap = 12;A removed line, an added line and the context around them.
A hunk
export function Button({ children, ...props }) {Removed:
return <button className="rounded-md px-3">{children}</button>;Added:
return (Added:
<button className="rounded-full px-3" {...props}>Added:
{children}Added:
</button>Added:
);}Several rows in a scrollable, focusable frame.
Usage
import { DiffRow } from "@/components/ui/diff-row"
<DiffRow kind="add" newLine={13}>{"const gap = 12;"}</DiffRow>Props
| Prop | Type | Default | Description |
|---|---|---|---|
| kind | "add" | "remove" | "context" | "context" | How the line changed. |
| oldLine | number | Line number in the old file. | |
| newLine | number | Line number in the new file. | |
| children | ReactNode | The code on that line, without the sign. |
Source
diff-row/diff-row.tsx
import * as React from "react";
import { cn } from "@/lib/utils";
type Kind = "add" | "remove" | "context";
type DiffRowProps = Omit<React.ComponentProps<"div">, "children"> & {
/** Added, removed or unchanged. */
kind?: Kind;
/** Line number in the old file. Leave out on added lines. */
oldLine?: number;
/** Line number in the new file. Leave out on removed lines. */
newLine?: number;
/** The line of code, without the leading plus or minus. */
children: React.ReactNode;
};
const SIGN = { add: "+", remove: "-", context: "" } as const;
const LABEL = { add: "Added: ", remove: "Removed: ", context: "" } as const;
const KIND: Record<Kind, string> = {
add: "bg-[color-mix(in_oklab,var(--code-string)_12%,transparent)] text-(--code-name) shadow-[inset_2px_0_0_var(--code-string)]",
remove:
"bg-[color-mix(in_oklab,var(--code-comment)_14%,transparent)] text-foreground/70 shadow-[inset_2px_0_0_var(--code-comment)]",
context: "text-(--code-plain)",
};
/**
* One line of a diff. Added lines take the code green, removed lines the code comment tone,
* and the sign column carries the meaning, so colour is never the only cue.
* Stack rows inside a wrapper with a border and overflow-x-auto.
*/
export function DiffRow({ kind = "context", oldLine, newLine, className, children, ...props }: DiffRowProps) {
return (
<div
data-slot="diff-row"
data-kind={kind}
className={cn("flex min-w-max font-mono text-xs leading-6", KIND[kind], className)}
{...props}
>
<span aria-hidden="true" className="w-9 shrink-0 pr-2 text-right text-foreground/70 tabular-nums select-none">
{oldLine}
</span>
<span aria-hidden="true" className="w-9 shrink-0 pr-2 text-right text-foreground/70 tabular-nums select-none">
{newLine}
</span>
<span aria-hidden="true" className="w-4 shrink-0 select-none">
{SIGN[kind]}
</span>
<span className="sr-only">{LABEL[kind]}</span>
<code className="pr-3 whitespace-pre">{children}</code>
</div>
);
}
For AI agents
Hand your agent this page, the markdown at /components/diff-row/prompt.md, or the Copy AI prompt button above.
When to use
- Showing a code change in docs, release notes or review tools
- Before and after snippets
When not to use
- Plain code listings: use a snippet card
- Word level or side by side diffs, which need more than rows
Agent notes
- This is a code view, so it uses the --code-* tokens: green for added lines, the comment tone for removed lines
- The sign column and a screen reader prefix carry the meaning, never colour alone
- Rows are min-w-max; wrap them in a box with overflow-x-auto and, if it scrolls, tabIndex 0, role region and an aria-label
- Whitespace is preserved, so pass indentation inside the string
const gap = 8;