Diff Table

Marks the cells that actually differ in a comparison table and shows blanks as "Not stated", so people don't pick the wrong plan.

StopsPicked the wrong plan from a comparison

Installation

pnpm dlx shadcn@latest add https://bollard-ui.vercel.app/r/diff-table.json

The files land in your project. They're yours to edit.

Usage

diff-table-usage.tsx
import { DiffTable, type DiffColumn, type DiffRow } from "@/components/ui/diff-table"

return <DiffTable columns={columns} rows={rows} caption="Plan comparison" defaultDifferencesOnly className="max-w-lg" />

What it catches

  • Real differences hidden among identical rows
  • Formatting-only differences that aren't real (case, spaces, units spacing)
  • Blank cells that would be read as "No"

Props

DiffTable props
PropTypeDefault
columns* (required)

The things being compared, left to right.

DiffColumn[]—
rows* (required)

The attributes, top to bottom.

DiffRow[]—
differencesOnly

Controlled "show only differences" state.

boolean—
defaultDifferencesOnly

Initial state when uncontrolled.

booleanfalse
onDifferencesOnlyChange

Called when the toggle changes.

(differencesOnly: boolean) => void—
caption

Accessible caption of the table.

string"Comparison"
toggleLabel

Label of the toggle.

string"Show only differences"
missingLabel

Text in a cell with no value.

string"Not stated"
missingNote

Note under a row with missing values. Receives the column labels that are missing.

(columns: string) => string(names) => `Not stated for ${names}. Don’t read it as “No”.`
yesLabel

Screen reader text for `true` and `false`.

string"Included"
noLabel

Text read for a No cell.

string"Not included"
hiddenNote

Line under a filtered table. Receives how many identical rows are hidden.

(count: number) => string(count) => `${count} identical ${count === 1 ? "row" : "r…
sameCountLabel

Count beside the toggle. Receives how many rows are identical.

(count: number) => string(count) => `(${count} same)`
showAllLabel

Button that turns the filter off. Receives the total number of rows.

(count: number) => string(count) => `Show all ${count}`
differsLabel

Screen reader text after a value that stands out.

string"(differs)"
locale

When set, numbers in cells are formatted for this locale (1000 → "1,000"). Left unset they print as given.

string—

* required. Also accepts the native props of <div> (className, aria-*, data-*…), passed through to it.

Accessibility

  • A real <table> with a <caption>
  • The filter is a role="switch" with aria-checked
  • Yes/No cells use text plus icons; differing cells say so in text
  • The hidden-rows note is aria-live="polite"

Only what the source does. Test with your own screen reader and content before shipping.

Limitations

  • Normalisation is simple; values that mean the same in different words still show as different
  • Rows with blank cells stay visible even when showing only differences

What it stops

Picked the wrong plan from a comparison

Also known as

comparison table · pricing table · compare plans · product compare · spec sheet · differences · feature matrix · pricing

Dependencies

Pairs well with