Skip to content
goodtabs UI
BrowseBrowse components

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

Hand-built UI compared with goodtabs UI
Compared onBy handgoodtabs UI
SetupA dayOne command
Dark modePer componentBuilt in
Keyboard supportOften missedTested

Hand-built against the library, three rows.

Check marks

Feature support, free against Pro
Compared onFreePro
All componentsYesYes
Synced collectionsNoYes
Skill packsNoYes
Local saved listYesYes

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

PropTypeDefaultDescription
rows{ label: string; without: string | boolean; with: string | boolean }[]One row per thing compared. A boolean renders a check or a dash.
withoutLabelstring"Without"Heading of the left value column.
withLabelstring"With"Heading of the right value column.
captionstringCaption 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