A semantic feature matrix with a highlighted choice and accessible boolean values.
| Feature | Starter | TeamFor growing groups |
|---|---|---|
| Members | 3 | Unlimited |
| Shared projects | ||
| Advanced roles |
import { ComparisonTable } from "@/components/ballmac/comparison-table"
export default function ComparisonTableDemo() {
return (
<ComparisonTable
className="w-full max-w-xl"
caption="Compare workspace plans"
columns={[
{ key: "starter", title: "Starter" },
{
key: "team",
title: "Team",
description: "For growing groups",
featured: true,
},
]}
rows={[
{ label: "Members", values: { starter: "3", team: "Unlimited" } },
{ label: "Shared projects", values: { starter: true, team: true } },
{ label: "Advanced roles", values: { starter: false, team: true } },
]}
/>
)
}Installation
$ pnpm dlx shadcn@latest add @ballmac/comparison-tableInstall the dependencies.
$ pnpm add lucide-reactAdd the Ballmac items it builds on.
$ pnpm dlx shadcn@latest add @ballmac/i18nCopy the source into your project.
components/ballmac/comparison-table.tsx// Ballmac UI: Comparison Table. https://ui.ballmac.com/components/comparison-table "use client" import * as React from "react" import { Check, Minus, X } from "lucide-react" import { cn } from "@/lib/utils" import { useMessages } from "@/lib/ballmac/i18n" type ComparisonColumn = { /** Stable key. */ key: string /** Display heading. */ title: string /** Optional short description. */ description?: string /** Highlight as the recommended choice. */ featured?: boolean } type ComparisonRow = { /** Feature name. */ label: string /** Optional extra context. */ description?: string /** Values keyed by column. Booleans render as available/unavailable. */ values: Record< string, React.ReactNode | boolean > } type ComparisonTableProps = React.ComponentProps<"div"> & { /** Accessible table caption. */ caption: string /** Compared choices. */ columns: ComparisonColumn[] /** Features or criteria. */ rows: ComparisonRow[] } function ComparisonTable({ className, caption, columns, rows, ...props }: ComparisonTableProps) { const msg = useMessages() return ( <div data-slot="comparison-table" className={cn( "relative w-full max-w-full overflow-x-auto rounded-xl border border-border", className, )} tabIndex={0} role="region" aria-label={caption} {...props} > <table className="w-full min-w-full border-collapse text-sm"> <caption className="sr-only">{caption}</caption> <thead> <tr className="bg-muted/50"> <th scope="col" className="text-muted-foreground min-w-28 p-3 text-start font-medium sm:p-4" > {msg("comparison-table.feature", "Feature")} </th> {columns.map((column) => ( <th key={column.key} scope="col" className={cn( "min-w-24 p-3 text-center font-semibold sm:p-4", column.featured && "bg-primary/5 text-primary", )} > {column.title} {column.description && ( <span className="text-muted-foreground mt-1 block text-xs font-normal"> {column.description} </span> )} </th> ))} </tr> </thead> <tbody> {rows.map((row) => ( <tr key={row.label} className="border-t border-border"> <th scope="row" className="p-3 text-start font-medium sm:p-4"> {row.label} {row.description && ( <span className="text-muted-foreground mt-1 block text-xs font-normal"> {row.description} </span> )} </th> {columns.map((column) => { const value = row.values[column.key] return ( <td key={column.key} className={cn( "p-3 text-center sm:p-4", column.featured && "bg-primary/5", )} > {typeof value === "boolean" ? ( <span role="img" aria-label={value ? msg("comparison-table.available", "Available") : msg("comparison-table.unavailable", "Unavailable")} className="inline-flex items-center justify-center gap-1"> {value ? ( <Check aria-hidden="true" className="text-primary size-4" /> ) : ( <X aria-hidden="true" className="text-muted-foreground size-4" /> )} </span> ) : ( (value ?? ( <Minus aria-label={msg("comparison-table.notSpecified", "Not specified")} className="text-muted-foreground mx-auto size-4" /> )) )} </td> ) })} </tr> ))} </tbody> </table> </div> ) } export { ComparisonTable, type ComparisonTableProps, type ComparisonColumn, type ComparisonRow, }Update the import paths to match your project setup.
Usage
import { ComparisonTable } from "@/components/ballmac/comparison-table"The full example is in the Code tab above.
Examples
States and variants
| Feature | CSV | |
|---|---|---|
| Editable data | ||
| Print ready |
import { ComparisonTable } from "@/components/ballmac/comparison-table"
export default function ComparisonTableStates() {
return (
<ComparisonTable
className="w-full max-w-xl"
caption="Compare export formats"
columns={[
{ key: "csv", title: "CSV" },
{ key: "pdf", title: "PDF" },
]}
rows={[
{ label: "Editable data", values: { csv: true, pdf: false } },
{ label: "Print ready", values: { csv: false, pdf: true } },
]}
/>
)
}API reference
| Prop | Type | Default |
|---|---|---|
caption*Accessible table caption. | string | — |
columns*Compared choices. | ComparisonColumn[] | — |
rows*Features or criteria. | ComparisonRow[] | — |
Also accepts the standard attributes of its root element.
Accessibility
| Key | Action |
|---|---|
| Tab / Arrow keys | Focuses and scrolls the comparison region when narrow |
Use with AI
A semantic feature matrix with a highlighted choice and accessible boolean values. With the shadcn MCP server set up (guide), ask your agent:
Add the Ballmac UI Comparison Table (@ballmac/comparison-table) to this project with the shadcn MCP, then use it where it fits.
Use it for
- Compare capabilities across choices
Not for
- Use Description List for one object
Registry JSON: https://ui.ballmac.com/r/comparison-table.json
Credits
Free to use in personal and commercial projects.
- npm
- lucide-react
- Registry
- @ballmac/i18nshadcn/utils
Pairs well with
Description List
A semantic key-value surface with responsive split and stacked layouts.
Activity Feed
A compact actor and action feed with timestamps and an empty state.
Audio Player
A podcast-style audio player: play and pause, skip back and forward, a scrubber with chapter ticks, a named current chapter and a speed control. Works with a real file or as a silent demo.
Avatar Circles
Overlapping avatars that spread on hover, lift individually with a name and role tooltip, show presence, fall back to initials and end in a +N count that can be a button.