Comparison table
A two-column table of without against with, in real table markup.
npx shadcn@latest add https://ui.goodtabs.co/r/comparison-table.json
With the @goodtabs namespace in components.json: npx shadcn@latest add @goodtabs/comparison-table
| Compared on | By hand | goodtabs UI |
|---|---|---|
| Setup | A day | One command |
| Dark mode | Per component | Built in |
| Keyboard support | Often missed | Tested |
Hand-built against the library, three rows.
Check marks
| Compared on | Free | Pro |
|---|---|---|
| All components | Yes | Yes |
| Synced collections | No | Yes |
| Skill packs | No | Yes |
| Local saved list | Yes | Yes |
Booleans render a check or a dash with screen reader text.
Usage
import { ComparisonTable } from "@/components/comparison-table"
<ComparisonTable caption="By hand against the library" rows={[{ label: "Setup", without: "A day", with: "One command" }]} />Props
| Prop | Type | Default | Description |
|---|---|---|---|
| rows | { label: string; without: string | boolean; with: string | boolean }[] | One row per thing compared. A boolean renders a check or a dash. | |
| withoutLabel | string | "Without" | Heading of the left value column. |
| withLabel | string | "With" | Heading of the right value column. |
| caption | string | Caption kept for screen readers. |
Source
comparison-table/comparison-table.tsx
import * as React from "react";
import { CheckIcon, MinusIcon } from "lucide-react";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { cn } from "@/lib/utils";
export type ComparisonRow = {
/** What is being compared, for example "Setup time". */
label: string;
/** What happens without it. Text, or a boolean for a plain mark. */
without: string | boolean;
/** What happens with it. Text, or a boolean for a plain mark. */
with: string | boolean;
};
type ComparisonTableProps = Omit<React.ComponentProps<typeof Table>, "children"> & {
rows: ComparisonRow[];
/** Column heading for the left value column. */
withoutLabel?: string;
/** Column heading for the right value column. */
withLabel?: string;
/** Table caption, kept for screen readers. */
caption?: string;
};
function Value({ value }: { value: string | boolean }) {
if (typeof value === "string") return <>{value}</>;
return value ? (
<>
<CheckIcon aria-hidden className="size-4 text-foreground" />
<span className="sr-only">Yes</span>
</>
) : (
<>
<MinusIcon aria-hidden className="size-4 text-muted-foreground" />
<span className="sr-only">No</span>
</>
);
}
/**
* A two-column comparison, without against with, in real table markup.
* Server component. The "with" column is set in foreground, the other in muted, so the eye lands on the win.
*/
export function ComparisonTable({
rows,
withoutLabel = "Without",
withLabel = "With",
caption,
className,
...props
}: ComparisonTableProps) {
return (
<Table data-slot="comparison-table" className={cn("min-w-0", className)} {...props}>
{caption && <caption className="sr-only">{caption}</caption>}
<TableHeader>
<TableRow>
<TableHead scope="col">
<span className="sr-only">Compared on</span>
</TableHead>
<TableHead scope="col" className="whitespace-normal text-muted-foreground">
{withoutLabel}
</TableHead>
<TableHead scope="col" className="whitespace-normal text-foreground">
{withLabel}
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{rows.map((row) => (
<TableRow key={row.label}>
<TableHead scope="row" className="font-medium whitespace-normal text-foreground">
{row.label}
</TableHead>
<TableCell className="whitespace-normal text-muted-foreground">
<span className="inline-flex items-center">
<Value value={row.without} />
</span>
</TableCell>
<TableCell className="font-medium whitespace-normal text-foreground">
<span className="inline-flex items-center">
<Value value={row.with} />
</span>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
);
}
For AI agents
Hand your agent this page, the markdown at /components/comparison-table/prompt.md, or the Copy AI prompt button above.
When to use
- Showing what changes with a product, row by row
- A free against Pro matrix of two columns
- Landing pages where bullets do not make the contrast clear
When not to use
- More than two options, which need a pricing table
- Long prose in cells
- Data tables with sorting
Agent notes
- Server component on the shared table primitive; scope attributes are set on every header
- Right column is foreground and weighted, left is muted, so the win reads first
- Boolean values render icons with sr-only Yes or No text
- Wraps in horizontal scroll below 20rem via the table container