Navigation
Comparison
Options side by side in a real table, so every value is read with its option and feature.
When to use it: Use it to weigh plans, products, or alternatives against the same list of features.
Example
import { CheckIcon, MinusIcon } from "@heroicons/react/16/solid";
import {
Comparison,
ComparisonBody,
ComparisonFeature,
ComparisonHeader,
ComparisonOption,
ComparisonRow,
ComparisonValue,
VisuallyHidden,
} from "@comp0/react";
const plans = [
{ value: "free", title: "Free", projects: "3", support: false, sso: false },
{ value: "team", title: "Team", projects: "Unlimited", support: true, sso: false },
{ value: "business", title: "Business", projects: "Unlimited", support: true, sso: true },
];
function Mark({ on }: { on: boolean }) {
if (on) return <CheckIcon className="size-4 text-teal-700 dark:text-teal-300" />;
return <MinusIcon className="size-4 text-zinc-400" />;
}
export function Example() {
return (
<div className="max-w-full overflow-x-auto">
<Comparison
aria-label="Plans compared"
className="w-full max-w-xl border-collapse text-left text-base sm:text-sm"
>
<ComparisonHeader>
<tr>
<th scope="col">
<VisuallyHidden>Feature</VisuallyHidden>
</th>
{plans.map((plan) => (
<ComparisonOption
key={plan.value}
value={plan.value}
recommended={plan.value === "team"}
className="px-3 py-2 font-semibold text-zinc-950 data-recommended:text-teal-700 dark:text-white dark:data-recommended:text-teal-300"
>
{plan.title}
</ComparisonOption>
))}
</tr>
</ComparisonHeader>
<ComparisonBody>
<ComparisonRow className="border-t border-zinc-950/10 dark:border-white/10">
<ComparisonFeature className="px-3 py-2 font-medium text-zinc-700 dark:text-zinc-300">
Projects
</ComparisonFeature>
{plans.map((plan) => (
<ComparisonValue
key={plan.value}
option={plan.value}
className="px-3 py-2 text-zinc-600 data-recommended:bg-teal-50 data-recommended:text-zinc-950 dark:text-zinc-400 dark:data-recommended:bg-teal-400/10 dark:data-recommended:text-white"
>
{plan.projects}
</ComparisonValue>
))}
</ComparisonRow>
<ComparisonRow className="border-t border-zinc-950/10 dark:border-white/10">
<ComparisonFeature className="px-3 py-2 font-medium text-zinc-700 dark:text-zinc-300">
Priority support
</ComparisonFeature>
{plans.map((plan) => (
<ComparisonValue
key={plan.value}
option={plan.value}
included={plan.support}
className="px-3 py-2 text-zinc-600 data-recommended:bg-teal-50 dark:text-zinc-400 dark:data-recommended:bg-teal-400/10"
>
<Mark on={plan.support} />
</ComparisonValue>
))}
</ComparisonRow>
<ComparisonRow className="border-t border-zinc-950/10 dark:border-white/10">
<ComparisonFeature className="px-3 py-2 font-medium text-zinc-700 dark:text-zinc-300">
Single sign-on
</ComparisonFeature>
{plans.map((plan) => (
<ComparisonValue
key={plan.value}
option={plan.value}
included={plan.sso}
className="px-3 py-2 text-zinc-600 data-recommended:bg-teal-50 dark:text-zinc-400 dark:data-recommended:bg-teal-400/10"
>
<Mark on={plan.sso} />
</ComparisonValue>
))}
</ComparisonRow>
</ComparisonBody>
</Comparison>
</div>
);
}Anatomy
Dashed frames are invisible state providers; shaded shapes own real DOM. Numbered pins match the list below.
A wireframe sketch of the assembled component. Each numbered marker matches a part in the list that follows.
Comparison
Native table that holds the whole comparison. Owns a DOM element.
ComparisonHeader
Native thead holding the row of option headers. Owns a DOM element.
ComparisonOption
Column header naming one option. Owns a DOM element.
ComparisonBody
Native tbody holding one row per feature. Owns a DOM element.
ComparisonRow
Native tr that detects whether its values differ. Owns a DOM element.
ComparisonFeature
Row header (th with scope=row) naming one feature. Owns a DOM element.
ComparisonValue
Cell holding the value of a feature for one option. Owns a DOM element.
Step by step
- 1
Add the main part
Start Comparison with a name, then add one ComparisonOption per option in the header row.
- 2
Add the supporting parts
Add a ComparisonRow per feature in ComparisonBody, with a ComparisonFeature and one ComparisonValue per option.
- 3
Make the behavior clear
Mark the suggested option with recommended and style the data attributes; use included for yes or no values so they have a text alternative.
Exampletsx <Comparison aria-label="Plans"> <ComparisonHeader> <tr> <th scope="col">Feature</th> <ComparisonOption value="pro" recommended> Pro </ComparisonOption> </tr> </ComparisonHeader> <ComparisonBody> <ComparisonRow> <ComparisonFeature>Support</ComparisonFeature> <ComparisonValue option="pro" included> ✓ </ComparisonValue> </ComparisonRow> </ComparisonBody> </Comparison>;
Keyboard
Forms and accessibility
No form behavior; a comparison presents data and submits nothing.
Accessibility checklist
- It renders a native table, so screen readers announce the option and the feature for every value.
- Always name the table with a caption child, aria-label, or aria-labelledby; a warning appears in development without one.
- Give the empty corner cell visible or visually hidden text such as Feature.
- Recommendations and yes or no values have text, not only color or icons, so none depend on sight.
- Highlight with data-recommended and data-differs rather than adding text that repeats.
API reference
import { Comparison, ComparisonBody, ComparisonFeature, ComparisonHeader, ComparisonOption, ComparisonRow, ComparisonValue } from "@comp0/react";Comparison
DOM elementNative table that holds the whole comparison.
| Prop | Type | Description |
|---|---|---|
aria-label | string | Names the table when there is no native caption child or aria-labelledby. |
as | ElementType | Renders another element in place of the default; Fragment merges the props into its single child. |
ComparisonHeader
DOM elementNative thead holding the row of option headers.
ComparisonOption
DOM elementColumn header naming one option.
| Prop | Type | Description |
|---|---|---|
value | string | Identity that pairs the column with its ComparisonValue cells. |
recommended | boolean | Marks the suggested option; announced as hidden text and exposed as data-recommended. |
recommendedLabel | string | The text announced after the name of a recommended option; defaults to Recommended. |
ComparisonBody
DOM elementNative tbody holding one row per feature.
ComparisonRow
DOM elementNative tr that detects whether its values differ.
ComparisonFeature
DOM elementRow header (th with scope=row) naming one feature.
ComparisonValue
DOM elementCell holding the value of a feature for one option.
| Prop | Type | Description |
|---|---|---|
option | string | The value of the ComparisonOption this cell belongs to. |
included | boolean | A yes or no value: adds the text Included or Not included and hides any children (the visual mark) from assistive technology. |
label | string | Overrides the text alternative of an included value. |
Style hooks
Attributes that appear while a state is true. Target them with Tailwind data variants such as data-open:bg-zinc-100, or with any CSS selector.
ComparisonOption
| Style hook | Meaning |
|---|---|
[data-recommended] | The option is recommended; its cells share the attribute so a whole column can be highlighted. |
ComparisonRow
| Style hook | Meaning |
|---|---|
[data-differs] | The values in the row are not all the same. |
ComparisonValue
| Style hook | Meaning |
|---|---|
[data-recommended] | The option is recommended; its cells share the attribute so a whole column can be highlighted. |
[data-differs] | The values in the row are not all the same. |
[data-included] | An included value that is true. |
[data-option] | The value of the option the cell belongs to. |