Skip to content
← Blocks

Product Comparison

A complete product comparison section, ready to adapt to your product.

Open ↗

Make it yours.

Install the editable source and its dependencies into your React + Tailwind project.

Scroll horizontally for long lines
pnpm dlx shadcn@4.0.8 add http://localhost:3000/r/product-comparison.json

Use it

Scroll horizontally for long lines
"use client";
import { ProductComparison } from "@/components/jez-ui/blocks/product-comparison";

export default function Example() {
  return (
    <>
      <ProductComparison />
    </>
  );
}

Compose your own

These styled parts are included in the same install. Use children to build your layout; add className only when you want an override.

Scroll horizontally for long lines
"use client";
import {
  ProductComparison,
  ProductComparisonHeading,
  ProductComparisonTable,
} from "@/components/jez-ui/blocks/product-comparison";

export default function Example() {
  return (
    <ProductComparison>
      <ProductComparisonHeading />
      <ProductComparisonTable />
    </ProductComparison>
  );
}
Scroll horizontally for long lines
import { ProductComparison, ProductComparisonTitle, ProductComparisonList, ProductComparisonHeading, ProductComparisonTable, ProductComparisonTableHeader, ProductComparisonRow, ProductComparisonHead, ProductComparisonTableBody, ProductComparisonCell } from "@/components/jez-ui/blocks/product-comparison";
Read the composition guide →

Props & types

Scroll horizontally for long lines
export type ProductComparisonOptions = {
  className?: string;
  features?: typeof ProductComparisonDefaultFeatures;
  heading?: React.ReactNode;
};

export type ProductComparisonProps = Omit<
  React.ComponentProps<"section">,
  keyof ProductComparisonOptions
> &
  ProductComparisonOptions;

ProductComparisonProps

Source

View 2 source files

registry/blocks/product-comparison.tsx

Scroll horizontally for long lines
"use client";
import * as React from "react";
import { cn } from "../ui/utils";
export type ProductComparisonOptions = {
  className?: string;
  features?: typeof ProductComparisonDefaultFeatures;
  heading?: React.ReactNode;
};
export type ProductComparisonProps = Omit<
  React.ComponentProps<"section">,
  keyof ProductComparisonOptions
> &
  ProductComparisonOptions;
const ProductComparisonDefaultFeatures = [
  ["Find context", "Search multiple tools", "Open the project"],
  ["Track a decision", "Ask in a message", "Read the decision log"],
  ["Plan next week", "Rebuild a spreadsheet", "Update the shared plan"],
];
function useProductComparisonModel({
  features = ProductComparisonDefaultFeatures,
  heading = "A clearer way to work.",
  className,
  children,
  ...rootProps
}: ProductComparisonProps) {
  return { features, heading, className, children, rootProps };
}
const ProductComparisonCompositionContext = React.createContext<ReturnType<
  typeof useProductComparisonModel
> | null>(null);
function useProductComparisonComposition() {
  const context = React.useContext(ProductComparisonCompositionContext);
  if (!context)
    throw new Error(
      "ProductComparison parts must be inside ProductComparison.",
    );
  return context;
}
export function ProductComparison(props: ProductComparisonProps) {
  const model = useProductComparisonModel(props);
  const { className, rootProps, children } = model;
  return (
    <ProductComparisonCompositionContext.Provider value={model}>
      <section {...rootProps} className={cn("overflow-x-auto py-8", className)}>
        {children !== undefined ? (
          children
        ) : (
          <>
            <ProductComparisonHeading />
            <ProductComparisonTable />
          </>
        )}
      </section>
    </ProductComparisonCompositionContext.Provider>
  );
}

export function ProductComparisonTitle({
  className,
  ...props
}: React.ComponentProps<"h2">) {
  return (
    <h2
      data-slot="product-comparison-title"
      className={cn("mb-6 text-3xl", className)}
      {...props}
    />
  );
}
export function ProductComparisonList({
  className,
  ...props
}: React.ComponentProps<"table">) {
  return (
    <table
      data-slot="product-comparison-list"
      className={cn("w-full text-left text-sm", className)}
      {...props}
    />
  );
}

export function ProductComparisonHeading({
  children,
  ...props
}: Partial<React.ComponentProps<typeof ProductComparisonTitle>> & {
  children?: React.ReactNode;
}) {
  const { heading } = useProductComparisonComposition();
  return (
    <ProductComparisonTitle {...props}>
      {children === undefined ? heading : children}
    </ProductComparisonTitle>
  );
}
export function ProductComparisonTable({
  children,
  ...props
}: Partial<React.ComponentProps<typeof ProductComparisonList>> & {
  children?: React.ReactNode;
}) {
  const { features } = useProductComparisonComposition();
  return (
    <ProductComparisonList {...props}>
      {children === undefined ? (
        <>
          <caption className="sr-only">Workflow comparison</caption>
          <ProductComparisonTableHeader>
            <ProductComparisonRow>
              <ProductComparisonHead>Task</ProductComparisonHead>
              <ProductComparisonHead>Scattered workflow</ProductComparisonHead>
              <ProductComparisonHead>Shared workspace</ProductComparisonHead>
            </ProductComparisonRow>
          </ProductComparisonTableHeader>
          <ProductComparisonTableBody>
            {features.map((r) => (
              <ProductComparisonRow key={r[0]}>
                {r.map((c, i) => (
                  <ProductComparisonCell key={i}>{c}</ProductComparisonCell>
                ))}
              </ProductComparisonRow>
            ))}
          </ProductComparisonTableBody>
        </>
      ) : (
        children
      )}
    </ProductComparisonList>
  );
}

export function ProductComparisonTableHeader({
  className,
  ...props
}: React.ComponentProps<"thead">) {
  return (
    <thead
      data-slot="product-comparison-tableheader"
      className={cn("", className)}
      {...props}
    />
  );
}
export function ProductComparisonRow({
  className,
  ...props
}: React.ComponentProps<"tr">) {
  return (
    <tr
      data-slot="product-comparison-row"
      className={cn("border-b border-border", className)}
      {...props}
    />
  );
}
export function ProductComparisonHead({
  className,
  ...props
}: React.ComponentProps<"th">) {
  return (
    <th
      data-slot="product-comparison-head"
      className={cn("p-3", className)}
      {...props}
    />
  );
}
export function ProductComparisonTableBody({
  className,
  ...props
}: React.ComponentProps<"tbody">) {
  return (
    <tbody
      data-slot="product-comparison-tablebody"
      className={cn("", className)}
      {...props}
    />
  );
}
export function ProductComparisonCell({
  className,
  ...props
}: React.ComponentProps<"td">) {
  return (
    <td
      data-slot="product-comparison-cell"
      className={cn("p-3", className)}
      {...props}
    />
  );
}

registry/ui/utils.ts

Scroll horizontally for long lines
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...values: ClassValue[]) {
  return twMerge(clsx(values));
}

Dependencies

clsx@2.1.1 · tailwind-merge@3.5.0

Accessibility & behaviour

Provide meaningful labels and preserve keyboard focus styles. Check contrast when customising theme colours.

Read the accessibility and performance guide →