Docs
Comparison Matrix

Comparison Matrix

A high-conversion competitive comparison section. Show why you're the better choice with categorized features, highlighted differentiators, and clear visual hierarchy.

Why developers choose Artifact UI

Stop overpaying for generic templates. Get production-ready sections built for SaaS.

Artifact UI
Best
Generic UI Kit
Build from Scratch

Core Value

SaaS-Specific Sections
Artifact UI
Generic UI Kit
Build from Scratch
Animated Components
Artifact UI50+
Generic UI Kit10-20
Build from Scratch0
Copy-Paste Ready
Artifact UI
Generic UI Kit
Build from Scratch

Developer Experience

TypeScript Support
Artifact UI
Generic UI KitVaries
Build from Scratch
Dark Mode Included
Artifact UI
Generic UI KitSometimes
Build from ScratchManual
Time to Ship
Artifact UI1 day
Generic UI Kit1 week
Build from Scratch2-3 weeks
Documentation Quality
Artifact UIExcellent
Generic UI KitGood
Build from ScratchNone

Pricing & Support

One-Time Cost
Artifact UI$29
Generic UI Kit$100+/year
Build from Scratch$5,000+ (your time)
Lifetime Updates
Artifact UI
Generic UI Kit
Build from Scratch
Community Support
Artifact UI
Generic UI Kit
Build from ScratchStack Overflow

Installation

Copy and paste the following code into your project.

"use client";
 
import { Check, X, Minus, LucideIcon, Sparkles, Info } from "lucide-react";
import { ReactNode, useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { cn } from "@/lib/utils";
import {
  Tooltip,
  TooltipContent,
  TooltipProvider,
  TooltipTrigger,
} from "@/components/ui/tooltip";
 
interface ComparisonFeature {
  name: string;
  description?: string;
  values: (boolean | string | ReactNode)[];
  highlight?: boolean; // Highlight this row as a key differentiator
}
 
interface ComparisonMatrixProps {
  headline?: string;
  description?: string;
  competitors: {
    name: string;
    tagline?: string;
    icon?: LucideIcon;
    isPrimary?: boolean;
    cta?: {
      label: string;
      href?: string;
    };
  }[];
  features?: ComparisonFeature[];
  categories?: {
    name: string;
    features: ComparisonFeature[];
  }[];
  className?: string;
}
 
export function ComparisonMatrix({
  headline = "See how we compare",
  description = "We're not just better. We're in a different league.",
  competitors,
  features,
  categories,
  className,
}: ComparisonMatrixProps) {
  const [hoveredRow, setHoveredRow] = useState<number | null>(null);
 
  const renderValue = (
    value: boolean | string | ReactNode,
    isPrimary: boolean,
  ) => {
    if (typeof value === "boolean") {
      return value ? (
        <div
          className={cn(
            "flex h-8 w-8 items-center justify-center rounded-full shadow-sm transition-all duration-300",
            isPrimary
              ? "bg-primary text-primary-foreground shadow-primary/25 ring-2 ring-primary/20"
              : "bg-emerald-500/10 text-emerald-600 dark:bg-emerald-500/20 dark:text-emerald-400",
          )}
        >
          <Check className="h-4 w-4 stroke-[3]" />
        </div>
      ) : (
        <div className="flex h-8 w-8 items-center justify-center rounded-full bg-muted/50 text-muted-foreground/50">
          <Minus className="h-4 w-4" />
        </div>
      );
    }
    return (
      <span
        className={cn(
          "text-sm font-medium",
          isPrimary ? "text-foreground font-semibold" : "text-muted-foreground",
        )}
      >
        {value}
      </span>
    );
  };
 
  const CompetitorCard = ({
    competitor,
    index,
  }: {
    competitor: (typeof competitors)[0];
    index: number;
  }) => {
    const Icon = competitor.icon;
    return (
      <div className="relative flex flex-col items-center p-6 pt-8">
        {competitor.isPrimary && (
          <div className="absolute -top-2 left-1/2 -translate-x-1/2">
            <div className="relative">
              <div className="absolute inset-0 rounded-full bg-emerald-500 opacity-60 blur-lg" />
              <span className="relative inline-flex items-center gap-1.5 rounded-full bg-emerald-500 px-4 py-1.5 text-[11px] font-semibold text-white shadow-lg shadow-emerald-500/30">
                {/* <Sparkles className="h-3.5 w-3.5" /> */}
                Best Choice
              </span>
            </div>
          </div>
        )}
 
        <div
          className={cn(
            "mb-4 flex h-12 w-12 items-center justify-center rounded-xl shadow-sm ring-1 transition-all duration-300",
            competitor.isPrimary
              ? "bg-primary/10 ring-primary/30 shadow-primary/20"
              : "bg-background ring-border",
          )}
        >
          {Icon ? (
            <Icon
              className={cn(
                "h-6 w-6",
                competitor.isPrimary ? "text-primary" : "text-muted-foreground",
              )}
            />
          ) : (
            <div className="h-6 w-6 rounded-full bg-muted" />
          )}
        </div>
 
        <h3
          className={cn(
            "text-lg font-bold tracking-tight",
            competitor.isPrimary ? "text-foreground" : "text-muted-foreground",
          )}
        >
          {competitor.name}
        </h3>
 
        {competitor.tagline && (
          <p className="mt-1 text-xs font-medium text-muted-foreground/80">
            {competitor.tagline}
          </p>
        )}
 
        {competitor.cta && (
          <a
            href={competitor.cta.href || "#"}
            className={cn(
              "mt-6 inline-flex h-9 items-center justify-center rounded-full px-4 text-xs font-semibold transition-all hover:scale-105 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
              competitor.isPrimary
                ? "bg-primary text-primary-foreground shadow-lg shadow-primary/20 hover:bg-primary/90"
                : "bg-secondary text-secondary-foreground hover:bg-secondary/80",
            )}
          >
            {competitor.cta.label}
          </a>
        )}
      </div>
    );
  };
 
  const renderFeatureRow = (feature: ComparisonFeature, index: number) => (
    <motion.div
      key={index}
      initial={{ opacity: 0, y: 10 }}
      whileInView={{ opacity: 1, y: 0 }}
      viewport={{ once: true }}
      transition={{ delay: index * 0.05 }}
      onMouseEnter={() => setHoveredRow(index)}
      onMouseLeave={() => setHoveredRow(null)}
      className={cn(
        "group relative grid items-center gap-4 px-6 py-4 transition-colors hover:bg-muted/40",
        competitors.length === 2
          ? "grid-cols-1 lg:grid-cols-3"
          : "grid-cols-1 lg:grid-cols-4",
        feature.highlight && "bg-primary/5 hover:bg-primary/10",
      )}
    >
      {/* Row Highlight Indicator */}
      {feature.highlight && (
        <div className="absolute left-0 top-0 bottom-0 w-1 bg-primary" />
      )}
 
      <div className="flex items-center gap-2">
        <span
          className={cn(
            "font-medium transition-colors",
            feature.highlight ? "text-foreground" : "text-muted-foreground group-hover:text-foreground",
          )}
        >
          {feature.name}
        </span>
        {feature.description && (
          <TooltipProvider>
            <Tooltip>
              <TooltipTrigger>
                <Info className="h-3.5 w-3.5 text-muted-foreground/50 hover:text-muted-foreground" />
              </TooltipTrigger>
              <TooltipContent>
                <p className="max-w-xs text-xs">{feature.description}</p>
              </TooltipContent>
            </Tooltip>
          </TooltipProvider>
        )}
      </div>
 
      {competitors.map((competitor, compIdx) => (
        <div
          key={compIdx}
          className={cn(
            "flex items-center justify-between lg:justify-center",
            competitor.isPrimary && "font-medium",
          )}
        >
          <span className="text-sm text-muted-foreground lg:hidden">
            {competitor.name}
          </span>
          {renderValue(feature.values[compIdx], competitor.isPrimary || false)}
        </div>
      ))}
    </motion.div>
  );
 
  return (
    <section className={cn("w-full py-12 sm:py-24", className)}>
      <div className="container px-4 md:px-6">
        <div className="mx-auto mb-16 max-w-3xl text-center">
          <h2 className="text-3xl font-bold tracking-tighter text-foreground sm:text-4xl md:text-5xl">
            {headline}
          </h2>
          {description && (
            <p className="mt-4 text-lg text-muted-foreground md:text-xl">
              {description}
            </p>
          )}
        </div>
 
        <div className="relative mx-auto max-w-7xl pt-12">
          {/* Sticky Header Background */}
          <div className="sticky top-20 z-20 hidden h-px w-full bg-gradient-to-r from-transparent via-border to-transparent lg:block" />
 
          {/* Comparison Table */}
          <div className="rounded-3xl border border-border bg-card/50 shadow-2xl backdrop-blur-sm overflow-hidden">
            {/* Header Row */}
            <div
              className={cn(
                "sticky top-0 z-10 hidden border-b border-border bg-card/95 backdrop-blur supports-[backdrop-filter]:bg-card/75 lg:grid pt-4",
                competitors.length === 2
                  ? "grid-cols-3"
                  : "grid-cols-4",
              )}
            >
              <div className="flex items-end p-6 pb-8">
                <span className="text-sm font-semibold uppercase tracking-wider text-muted-foreground">
                  Features
                </span>
              </div>
              {competitors.map((competitor, idx) => (
                <CompetitorCard key={idx} competitor={competitor} index={idx} />
              ))}
            </div>
 
            {/* Mobile Header */}
            <div className="border-b border-border p-6 lg:hidden">
              <div className="flex flex-col gap-4">
                {competitors.map((competitor, idx) => (
                  <div
                    key={idx}
                    className={cn(
                      "flex items-center justify-between rounded-lg border p-4",
                      competitor.isPrimary
                        ? "border-primary/50 bg-primary/5"
                        : "border-border bg-background",
                    )}
                  >
                    <div className="flex items-center gap-3">
                      {competitor.icon && (
                        <competitor.icon
                          className={cn(
                            "h-5 w-5",
                            competitor.isPrimary
                              ? "text-primary"
                              : "text-muted-foreground",
                          )}
                        />
                      )}
                      <span
                        className={cn(
                          "font-semibold",
                          competitor.isPrimary && "text-primary",
                        )}
                      >
                        {competitor.name}
                      </span>
                    </div>
                    {competitor.isPrimary && (
                      <span className="rounded-full bg-primary/10 px-2 py-0.5 text-[10px] font-bold uppercase text-primary">
                        Best
                      </span>
                    )}
                  </div>
                ))}
              </div>
            </div>
 
            {/* Feature Rows */}
            <div className="divide-y divide-border/50">
              {categories
                ? categories.map((category, catIdx) => (
                  <div key={catIdx} className="relative">
                    <div className="sticky top-[140px] z-0 bg-muted/20 px-6 py-3 backdrop-blur-sm lg:top-[180px]">
                      <h3 className="text-xs font-bold uppercase tracking-wider text-foreground">
                        {category.name}
                      </h3>
                    </div>
                    {category.features.map((feature, featIdx) =>
                      renderFeatureRow(feature, featIdx),
                    )}
                  </div>
                ))
                : features?.map((feature, index) =>
                  renderFeatureRow(feature, index),
                )}
            </div>
          </div>
        </div>
      </div>
    </section>
  );
}

Update the import paths to match your project setup.

Features

  • Competitive Positioning - Visually highlights your product as the winner with a "Best Choice" badge and premium styling.
  • Sticky Headers - Competitor names stick to the top of the table while scrolling, ensuring context is never lost.
  • Glassmorphism Effects - Modern, translucent backgrounds for a high-end SaaS feel.
  • Smooth Animations - Rows animate in on scroll, and hover effects guide the user's eye.
  • Feature Categories - Group features into logical sections (Core Value, Pricing, etc.) with sticky category headers.
  • Highlight Key Differentiators - Mark your strongest features with visual emphasis (primary color border and background).
  • Tooltips - Add detailed descriptions to features using the description prop.
  • Mobile Responsive - Adapts gracefully to smaller screens with a simplified card view.
  • Dark Mode Ready - Looks stunning in both light and dark themes.

Usage

Basic Example

import { ComparisonMatrix } from "@/components/ui/comparison-matrix";
import { Zap, Box, Code } from "lucide-react";
 
export default function Page() {
  return (
    <ComparisonMatrix
      headline="Why choose us"
      description="See how we stack up against the competition"
      competitors={[
        {
          name: "Your Product",
          tagline: "Built for speed",
          icon: Zap,
          isPrimary: true,
          cta: {
            label: "Get Started",
            href: "/signup",
          },
        },
        {
          name: "Competitor A",
          tagline: "Generic solution",
          icon: Box,
        },
        {
          name: "DIY Approach",
          icon: Code,
        },
      ]}
      categories={[
        {
          name: "Core Features",
          features: [
            {
              name: "Real-time Sync",
              values: [true, false, false],
              highlight: true, // Highlight this as a key differentiator
            },
            {
              name: "Users",
              values: ["Unlimited", "5", "Manual"],
            },
          ],
        },
        {
          name: "Pricing",
          features: [
            {
              name: "Cost",
              values: ["$29", "$99/mo", "$5,000+"],
            },
          ],
        },
      ]}
    />
  );
}

Without Categories

You can also use a flat feature list without categories:

<ComparisonMatrix
  competitors={[...]}
  features={[
    {
      name: "Feature 1",
      description: "Optional description",
      values: [true, false, true],
    },
    {
      name: "Feature 2",
      values: ["Value 1", "Value 2", "Value 3"],
    },
  ]}
/>

Props

PropTypeDescription
headlinestringSection headline
descriptionstringSection description
competitorsCompetitor[]Array of competitors to compare
featuresComparisonFeature[]Optional flat list of features (use if not using categories)
categoriesCategory[]Optional grouped features by category
classNamestringAdditional CSS classes

Competitor Object

PropTypeDescription
namestringName of the competitor
taglinestringOptional tagline/description
iconLucideIconOptional icon component
isPrimarybooleanIf true, highlights this as your product
cta{ label: string, href?: string }Optional call-to-action button

Category Object

PropTypeDescription
namestringCategory name (e.g., "Core Features")
featuresComparisonFeature[]Array of features in this category

ComparisonFeature Object

PropTypeDescription
namestringName of the feature
descriptionstringOptional description/tooltip
values(boolean | string | ReactNode)[]Values for each competitor (in order)
highlightbooleanIf true, visually emphasizes this row