Two-plan diff pricing comparison table for React and Tailwind CSS (shadcn/ui) that compares any two plans and shows only the rows that change, with color-coded upgrades and downgrades. It is built to help people decide on an upgrade in seconds instead of scanning a full feature matrix. Pick plans A and B from the chips, swap them, and read a summary of the price difference with upgrade and downgrade counts; shared rows stay collapsed until you expand them. Values are compared automatically for numbers, "Unlimited", booleans, and leading quantities, with an optional rank order for words and a lower-is-better flag for limits. Rows glide between the gain, lose, and same sections with a FLIP transition, and chart tokens tint the A/B chips and verdict cells. Supports controlled pair and visibility state, choose-plan callbacks, keyboard use, live-region summaries, reduced motion, and light and dark themes. Use it for SaaS pricing pages, upgrade dialogs, and in-app plan pickers. The included demo tours three sample pairs; the component makes no requests. Requires React and lucide-react, with no animation library.
Built with React and Tailwind. Copy the prompt and paste it into Claude Code, Cursor or Lovable: your agent adds Two-Plan Diff Pricing Comparison Table to your project and fits it to your theme.
Code
How Two-Plan Diff Pricing Comparison Table is used: the demo's code. The component's own source comes with a free account.
Usage.tsx
"use client";
import * as React from "react";
import { TwoPlanDiffPricingTable } from "@/components/ui/two-plan-diff-pricing-table";
import type { DiffGroup, DiffPair, DiffPlan } from "@/components/ui/two-plan-diff-pricing-table";
export const DIFF_PLANS: DiffPlan[] = [
{ id: "starter", name: "Starter", price: 0, actionLabel: "Switch to Starter" },
{ id: "pro", name: "Pro", price: 19, badge: "Popular", actionLabel: "Switch to Pro" },
{ id: "business", name: "Business", price: 49, actionLabel: "Switch to Business" },
{ id: "enterprise", name: "Enterprise", price: 99, actionLabel: "Talk to sales" },
];
export const DIFF_GROUPS: DiffGroup[] = [
{ id: "workspace", label: "Workspace", features: [
{ id: "projects", label: "Projects", values: { starter: "3", pro: "Unlimited", business: "Unlimited", enterprise: "Unlimited" } },
{ id: "members", label: "Team members", values: { starter: "1", pro: "10", business: "50", enterprise: "Unlimited" } },
{ id: "storage", label: "File storage", values: { starter: "5 GB", pro: "100 GB", business: "500 GB", enterprise: "2,000 GB" } },
] },
{ id: "workflow", label: "Workflow", features: [
{ id: "automations", label: "Automations", values: { starter: false, pro: true, business: true, enterprise: true } },
{ id: "support", label: "Support", values: { starter: "Community", pro: "Email", business: "Priority", enterprise: "Dedicated" }, rank: ["Community", "Email", "Priority", "Dedicated"] },
{ id: "export", label: "Export data", values: { starter: true, pro: true, business: true, enterprise: true } },
] },
{ id: "control", label: "Control", features: [
{ id: "sso", label: "SAML SSO", values: { starter: false, pro: false, business: true, enterprise: true } },
{ id: "audit", label: "Audit log retention", values: { starter: "7 days", pro: "30 days", business: "1 year", enterprise: "Unlimited" }, rank: ["7 days", "30 days", "1 year", "Unlimited"] },
{ id: "seats", label: "Minimum seats", values: { starter: 1, pro: 1, business: 5, enterprise: 25 }, better: "lower" },
{ id: "two-factor", label: "Two-factor auth", values: { starter: true, pro: true, business: true, enterprise: true } },
] },
];
const TOUR: DiffPair[] = [["starter", "pro"], ["pro", "business"], ["business", "enterprise"]];
type DemoProps = {
autoplay?: boolean;
bare?: boolean;
controlled?: boolean;
initialPair?: DiffPair;
showSame?: boolean;
};
export default function TwoPlanDiffDemo({ autoplay = true, bare = false, controlled = false, initialPair = TOUR[0], showSame = false }: DemoProps) {
const [pair, setPair] = React.useState(initialPair as DiffPair);
const [same, setSame] = React.useState(showSame);
const [choice, setChoice] = React.useState("");
return (
<main className="flex min-h-svh w-full items-center justify-center bg-background p-3 text-foreground">
<div className="w-full max-w-[720px]">
{!bare && (
<header className="mb-3 flex items-end justify-between gap-2 px-1">
<div>
<p className="mb-1 text-[9px] font-medium uppercase tracking-[0.2em] text-muted-foreground">Compare any two plans</p>
<h1 className="text-[22px] font-semibold leading-7 tracking-tight">Only the rows that change.</h1>
</div>
<span className="mb-1 hidden text-[10px] text-muted-foreground sm:block">Sample plans · billed monthly</span>
</header>
)}
<TwoPlanDiffPricingTable
plans={DIFF_PLANS}
groups={DIFF_GROUPS}
autoplay={autoplay}
autoplayPairs={TOUR}
autoplayInterval={2000}
defaultPair={initialPair}
defaultShowSame={showSame}
{...(controlled ? { pair, onPairChange: setPair, showSame: same, onShowSameChange: setSame } : {})}
onChoosePlan={(plan) => setChoice(plan.name)}
/>
{choice && <p role="status" data-demo-choice className="sr-only">{choice} selected.</p>}
</div>
</main>
);
}
















