Why developers choose Artifact UI
Stop overpaying for generic templates. Get production-ready sections built for SaaS.
Artifact UI
BestGeneric 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
descriptionprop. - ✅ 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
| Prop | Type | Description |
|---|---|---|
headline | string | Section headline |
description | string | Section description |
competitors | Competitor[] | Array of competitors to compare |
features | ComparisonFeature[] | Optional flat list of features (use if not using categories) |
categories | Category[] | Optional grouped features by category |
className | string | Additional CSS classes |
Competitor Object
| Prop | Type | Description |
|---|---|---|
name | string | Name of the competitor |
tagline | string | Optional tagline/description |
icon | LucideIcon | Optional icon component |
isPrimary | boolean | If true, highlights this as your product |
cta | { label: string, href?: string } | Optional call-to-action button |
Category Object
| Prop | Type | Description |
|---|---|---|
name | string | Category name (e.g., "Core Features") |
features | ComparisonFeature[] | Array of features in this category |
ComparisonFeature Object
| Prop | Type | Description |
|---|---|---|
name | string | Name of the feature |
description | string | Optional description/tooltip |
values | (boolean | string | ReactNode)[] | Values for each competitor (in order) |
highlight | boolean | If true, visually emphasizes this row |