Comparison

Side-by-side comparison layout for content or features. Install Comparison from the VLLNT UI registry with the shadcn CLI.

Report a bug

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

Source

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

Dependencies

  • @vllnt/ui@^0.3.0