Skip to content

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.

On this page

Example

Loading example…
Comparison.tsxtsx
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.

  1. Comparison

    Native table that holds the whole comparison. Owns a DOM element.

  2. ComparisonHeader

    Native thead holding the row of option headers. Owns a DOM element.

  3. ComparisonOption

    Column header naming one option. Owns a DOM element.

  4. ComparisonBody

    Native tbody holding one row per feature. Owns a DOM element.

  5. ComparisonRow

    Native tr that detects whether its values differ. Owns a DOM element.

  6. ComparisonFeature

    Row header (th with scope=row) naming one feature. Owns a DOM element.

  7. ComparisonValue

    Cell holding the value of a feature for one option. Owns a DOM element.

Step by step

  1. 1

    Add the main part

    Start Comparison with a name, then add one ComparisonOption per option in the header row.

  2. 2

    Add the supporting parts

    Add a ComparisonRow per feature in ComparisonBody, with a ComparisonFeature and one ComparisonValue per option.

  3. 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

Importtsx
import { Comparison, ComparisonBody, ComparisonFeature, ComparisonHeader, ComparisonOption, ComparisonRow, ComparisonValue } from "@comp0/react";

Comparison

DOM element

Native table that holds the whole comparison.

PropTypeDescription
aria-labelstringNames the table when there is no native caption child or aria-labelledby.
asElementTypeRenders another element in place of the default; Fragment merges the props into its single child.

ComparisonHeader

DOM element

Native thead holding the row of option headers.

ComparisonOption

DOM element

Column header naming one option.

PropTypeDescription
valuestringIdentity that pairs the column with its ComparisonValue cells.
recommendedbooleanMarks the suggested option; announced as hidden text and exposed as data-recommended.
recommendedLabelstringThe text announced after the name of a recommended option; defaults to Recommended.

ComparisonBody

DOM element

Native tbody holding one row per feature.

ComparisonRow

DOM element

Native tr that detects whether its values differ.

ComparisonFeature

DOM element

Row header (th with scope=row) naming one feature.

ComparisonValue

DOM element

Cell holding the value of a feature for one option.

PropTypeDescription
optionstringThe value of the ComparisonOption this cell belongs to.
includedbooleanA yes or no value: adds the text Included or Not included and hides any children (the visual mark) from assistive technology.
labelstringOverrides 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 hookMeaning
[data-recommended]The option is recommended; its cells share the attribute so a whole column can be highlighted.

ComparisonRow

Style hookMeaning
[data-differs]The values in the row are not all the same.

ComparisonValue

Style hookMeaning
[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.

Keep exploring