Skip to content
goodtabs UI
BrowseBrowse components

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;
Removed: const gap = 8;
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

PropTypeDefaultDescription
kind"add" | "remove" | "context""context"How the line changed.
oldLinenumberLine number in the old file.
newLinenumberLine number in the new file.
childrenReactNodeThe 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