Comparison
Side-by-side comparison layout for content or features. Install Comparison from the VLLNT UI registry with the shadcn CLI.
Side-by-side comparison layout for content or features. Part of the data family in VLLNT UI, it ships as a machine-readable registry entry — copy the source directly into your app with the shadcn CLI and own it, no runtime dependency on a component library.
Preview
Switch between light and dark to inspect the embedded Storybook preview.
Installation
Add Comparison to your project with the shadcn CLI. The source lands in your codebase, ready to adapt:
pnpm dlx shadcn@latest add https://ui.vllnt.ai/r/comparison.jsonSource
"use client";
import { ArrowRight, Check, Minus, X } from "lucide-react";
import type { ReactNode } from "react";
import type { HeadingTag } from "../../lib/types";
import { cn } from "../../lib/utils";
type ComparisonSide = {
items: string[];
title: string;
variant?: "bad" | "good" | "neutral";
};
export type ComparisonProps = {
after: ComparisonSide;
/** Heading tag for the title. Defaults to `h4`. */
as?: HeadingTag;
before: ComparisonSide;
title?: string;
};
const variantConfig = {
bad: {
className: "border-red-500/30 bg-red-500/5",
headerClass: "bg-red-500/10 text-red-700 dark:text-red-300",
icon: X,
iconClass: "text-red-500",
},
good: {
className: "border-green-500/30 bg-green-500/5",
headerClass: "bg-green-500/10 text-green-700 dark:text-green-300",
icon: Check,
iconClass: "text-green-500",
},
neutral: {
className: "border-border bg-muted/30",
headerClass: "bg-muted text-muted-foreground",
icon: Minus,
iconClass: "text-muted-foreground",
},
};
export function Comparison({
after,
as: Heading = "h4",
before,
title,
...rest
}: ComparisonProps & Record<string, unknown>) {
if (!before || !after) {
const hint =
"left" in rest || "right" in rest
? ' Did you mean "before" / "after" instead of "left" / "right"?'
: "";
console.error(
`[Comparison] Missing required props "before" and "after".${hint}`,
);
return null;
}
const beforeConfig = variantConfig[before.variant || "bad"];
const afterConfig = variantConfig[after.variant || "good"];
const BeforeIcon = beforeConfig.icon;
const AfterIcon = afterConfig.icon;
return (
<div className="my-6">
{title ? <Heading className="font-semibold mb-3">{title}</Heading> : null}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className={cn("rounded-lg border", beforeConfig.className)}>
<div
className={cn(
"px-4 py-2 rounded-t-lg font-medium text-sm",
beforeConfig.headerClass,
)}
>
{before.title}
</div>
<ul className="p-4 space-y-2">
{before.items.map((item) => (
<li className="flex items-start gap-2 text-sm" key={item}>
<BeforeIcon
className={cn(
"size-4 mt-0.5 flex-shrink-0",
beforeConfig.iconClass,
)}
/>
<span>{item}</span>
</li>
))}
</ul>
</div>
<div className={cn("rounded-lg border", afterConfig.className)}>
<div
className={cn(
"px-4 py-2 rounded-t-lg font-medium text-sm",
afterConfig.headerClass,
)}
>
{after.title}
</div>
<ul className="p-4 space-y-2">
{after.items.map((item) => (
<li className="flex items-start gap-2 text-sm" key={item}>
<AfterIcon
className={cn(
"size-4 mt-0.5 flex-shrink-0",
afterConfig.iconClass,
)}
/>
<span>{item}</span>
</li>
))}
</ul>
</div>
</div>
</div>
);
}
export type BeforeAfterProps = {
after: ReactNode;
before: ReactNode;
title?: string;
};
export function BeforeAfter({ after, before, title }: BeforeAfterProps) {
return (
<div className="my-6">
{title ? <h4 className="font-semibold mb-3">{title}</h4> : null}
<div className="grid grid-cols-1 md:grid-cols-[1fr_auto_1fr] gap-4 items-start">
<div className="rounded-lg border border-red-500/30 bg-red-500/5 overflow-hidden">
<div className="px-4 py-2 bg-red-500/10 text-red-700 dark:text-red-300 font-medium text-sm flex items-center gap-2">
<X className="size-4" />
Before
</div>
<div className="p-4 text-sm [&>pre]:my-0">{before}</div>
</div>
<div className="hidden md:flex items-center justify-center h-full">
<ArrowRight className="size-6 text-muted-foreground" />
</div>
<div className="rounded-lg border border-green-500/30 bg-green-500/5 overflow-hidden">
<div className="px-4 py-2 bg-green-500/10 text-green-700 dark:text-green-300 font-medium text-sm flex items-center gap-2">
<Check className="size-4" />
After
</div>
<div className="p-4 text-sm [&>pre]:my-0">{after}</div>
</div>
</div>
</div>
);
}
Stories
Explore every variant and state in the interactive Storybook:
Preview
Switch between light and dark to inspect the embedded Storybook preview.