Feature Matrix Pricing
Original · freefeature matrix pricing
Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.
npx shadcn@latest add https://webinnoventix.com/r/pricex-feature-matrix.json"use client";
import { useId, useRef, useState, type KeyboardEvent } from "react";
import { motion, useReducedMotion } from "motion/react";
type Billing = "monthly" | "annual";
type PlanId = "starter" | "growth" | "scale";
type CellValue = boolean | string;
interface Plan {
id: PlanId;
name: string;
tagline: string;
monthly: number;
annual: number;
cta: string;
featured?: boolean;
}
interface FeatureRow {
label: string;
hint?: string;
values: Record<PlanId, CellValue>;
}
interface FeatureGroup {
id: string;
name: string;
rows: FeatureRow[];
}
const PLANS: Plan[] = [
{
id: "starter",
name: "Starter",
tagline: "For solo builders shipping their first idea.",
monthly: 19,
annual: 15,
cta: "Start free",
},
{
id: "growth",
name: "Growth",
tagline: "For teams that need automation and real control.",
monthly: 49,
annual: 39,
cta: "Start 14-day trial",
featured: true,
},
{
id: "scale",
name: "Scale",
tagline: "For orgs with security and compliance needs.",
monthly: 99,
annual: 79,
cta: "Talk to sales",
},
];
const GROUPS: FeatureGroup[] = [
{
id: "core",
name: "Core workspace",
rows: [
{ label: "Active projects", values: { starter: "3", growth: "Unlimited", scale: "Unlimited" } },
{ label: "Team members", values: { starter: "2", growth: "15", scale: "Unlimited" } },
{ label: "File storage", values: { starter: "5 GB", growth: "100 GB", scale: "1 TB" } },
{
label: "Automations / month",
hint: "Trigger-based workflows that run without you.",
values: { starter: "100", growth: "5,000", scale: "Unlimited" },
},
],
},
{
id: "collab",
name: "Collaboration",
rows: [
{ label: "Real-time editing", values: { starter: true, growth: true, scale: true } },
{ label: "Guest access", values: { starter: false, growth: true, scale: true } },
{ label: "Comment threads", values: { starter: true, growth: true, scale: true } },
{ label: "Shared workspaces", values: { starter: "1", growth: "10", scale: "Unlimited" } },
],
},
{
id: "security",
name: "Security & admin",
rows: [
{ label: "SSO / SAML", values: { starter: false, growth: false, scale: true } },
{ label: "Audit logs", values: { starter: false, growth: true, scale: true } },
{ label: "Role-based access", values: { starter: false, growth: true, scale: true } },
{ label: "Data residency", values: { starter: false, growth: false, scale: true } },
],
},
{
id: "support",
name: "Support",
rows: [
{ label: "Community support", values: { starter: true, growth: true, scale: true } },
{ label: "Priority email", values: { starter: false, growth: true, scale: true } },
{ label: "Dedicated manager", values: { starter: false, growth: false, scale: true } },
{ label: "99.9% uptime SLA", values: { starter: false, growth: false, scale: true } },
],
},
];
const BILLING_OPTIONS: { id: Billing; label: string }[] = [
{ id: "monthly", label: "Monthly" },
{ id: "annual", label: "Annual" },
];
function CheckIcon() {
return (
<svg viewBox="0 0 20 20" fill="none" className="h-4 w-4" aria-hidden="true">
<path
d="M4 10.5 8 14.5 16 6"
stroke="currentColor"
strokeWidth="2.25"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
function MinusIcon() {
return (
<svg viewBox="0 0 20 20" fill="none" className="h-4 w-4" aria-hidden="true">
<path d="M5 10h10" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
</svg>
);
}
function ChevronIcon({ open }: { open: boolean }) {
return (
<svg
viewBox="0 0 20 20"
fill="none"
aria-hidden="true"
className={`h-4 w-4 shrink-0 transition-transform duration-300 ${open ? "rotate-180" : ""}`}
>
<path d="M5 7.5 10 12.5 15 7.5" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
}
function ArrowIcon() {
return (
<svg viewBox="0 0 20 20" fill="none" className="h-4 w-4" aria-hidden="true">
<path d="M4 10h11m0 0-4-4m4 4-4 4" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
}
export default function PricexFeatureMatrix() {
const reduce = useReducedMotion();
const uid = useId();
const [billing, setBilling] = useState<Billing>("annual");
const [open, setOpen] = useState<Record<string, boolean>>(() =>
Object.fromEntries(GROUPS.map((g) => [g.id, true])),
);
const segRefs = useRef<(HTMLButtonElement | null)[]>([]);
function toggleGroup(id: string) {
setOpen((prev) => ({ ...prev, [id]: !prev[id] }));
}
function handleSegKey(e: KeyboardEvent<HTMLDivElement>) {
const idx = BILLING_OPTIONS.findIndex((o) => o.id === billing);
let next = idx;
if (e.key === "ArrowRight" || e.key === "ArrowDown") next = (idx + 1) % BILLING_OPTIONS.length;
else if (e.key === "ArrowLeft" || e.key === "ArrowUp") next = (idx - 1 + BILLING_OPTIONS.length) % BILLING_OPTIONS.length;
else if (e.key === "Home") next = 0;
else if (e.key === "End") next = BILLING_OPTIONS.length - 1;
else return;
e.preventDefault();
setBilling(BILLING_OPTIONS[next].id);
segRefs.current[next]?.focus();
}
const enter = reduce
? {}
: {
initial: { opacity: 0, y: 20 },
whileInView: { opacity: 1, y: 0 },
viewport: { once: true, margin: "-60px" } as const,
};
return (
<section className="relative w-full overflow-hidden bg-white py-20 sm:py-28 dark:bg-slate-950">
<style>{`
@keyframes pxfm-float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-22px); }
}
@keyframes pxfm-shimmer {
0% { background-position: -160% 0; }
100% { background-position: 160% 0; }
}
.pxfm-orb { animation: pxfm-float 14s ease-in-out infinite; }
.pxfm-shine {
background-image: linear-gradient(110deg, rgba(255,255,255,0) 25%, rgba(255,255,255,0.65) 50%, rgba(255,255,255,0) 75%);
background-size: 200% 100%;
background-repeat: no-repeat;
animation: pxfm-shimmer 3.6s linear infinite;
}
@media (prefers-reduced-motion: reduce) {
.pxfm-orb, .pxfm-shine { animation: none !important; }
}
`}</style>
{/* Decorative background */}
<div aria-hidden="true" className="pointer-events-none absolute inset-0 overflow-hidden">
<div className="pxfm-orb absolute -top-24 left-1/4 h-72 w-72 rounded-full bg-gradient-to-br from-indigo-300/40 to-violet-300/30 blur-3xl dark:from-indigo-600/20 dark:to-violet-700/10" />
<div
className="pxfm-orb absolute -bottom-28 right-1/4 h-72 w-72 rounded-full bg-gradient-to-br from-sky-300/30 to-emerald-200/30 blur-3xl dark:from-sky-700/10 dark:to-emerald-700/10"
style={{ animationDelay: "-7s" }}
/>
</div>
<div className="relative mx-auto max-w-6xl px-4 sm:px-6 lg:px-8">
{/* Header */}
<motion.div className="mx-auto max-w-2xl text-center" {...enter} transition={{ duration: 0.5, ease: "easeOut" }}>
<span className="inline-flex items-center gap-2 rounded-full border border-indigo-200 bg-indigo-50 px-3 py-1 text-xs font-semibold uppercase tracking-wider text-indigo-700 dark:border-indigo-400/30 dark:bg-indigo-500/10 dark:text-indigo-300">
<span className="h-1.5 w-1.5 rounded-full bg-indigo-500" />
Pricing
</span>
<h2 className="mt-5 text-balance text-3xl font-bold tracking-tight text-slate-900 sm:text-4xl dark:text-white">
Compare every plan, feature by feature
</h2>
<p className="mx-auto mt-4 max-w-xl text-pretty text-base leading-relaxed text-slate-600 dark:text-slate-400">
PriceX scales from your first side project to a company-wide rollout. No hidden add-ons, no surprise
overages, cancel any time.
</p>
{/* Billing toggle */}
<div className="mt-8 flex flex-col items-center gap-3">
<div
role="radiogroup"
aria-label="Billing period"
onKeyDown={handleSegKey}
className="inline-flex items-center rounded-full border border-slate-200 bg-slate-100 p-1 dark:border-slate-800 dark:bg-slate-900"
>
{BILLING_OPTIONS.map((o, i) => {
const active = billing === o.id;
return (
<button
key={o.id}
ref={(el) => {
segRefs.current[i] = el;
}}
type="button"
role="radio"
aria-checked={active}
tabIndex={active ? 0 : -1}
onClick={() => setBilling(o.id)}
className={`relative inline-flex items-center gap-2 rounded-full px-4 py-1.5 text-sm font-medium transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-100 dark:focus-visible:ring-offset-slate-900 ${
active
? "bg-white text-slate-900 shadow-sm dark:bg-slate-700 dark:text-white"
: "text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200"
}`}
>
{o.label}
{o.id === "annual" && (
<span className="rounded-full bg-emerald-100 px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-wide text-emerald-700 dark:bg-emerald-500/15 dark:text-emerald-300">
-20%
</span>
)}
</button>
);
})}
</div>
<p className="text-xs text-slate-500 dark:text-slate-500">
14-day free trial on every plan. No credit card required.
</p>
</div>
</motion.div>
{/* Matrix */}
<motion.div
className="mt-12 rounded-3xl border border-slate-200 bg-white/70 p-2 shadow-sm backdrop-blur-sm sm:mt-14 sm:p-3 dark:border-slate-800 dark:bg-slate-900/40"
{...enter}
transition={{ duration: 0.5, ease: "easeOut", delay: reduce ? 0 : 0.1 }}
>
<div className="overflow-x-auto">
<table className="w-full min-w-[720px] border-collapse text-left">
<caption className="sr-only">Feature comparison across the Starter, Growth, and Scale plans</caption>
<thead>
<tr>
<th scope="col" className="w-2/5 px-3 pb-4 pt-3 text-left align-bottom sm:px-4">
<p className="text-xs font-semibold uppercase tracking-wider text-slate-400 dark:text-slate-500">
Compare plans
</p>
</th>
{PLANS.map((p) => {
const price = billing === "monthly" ? p.monthly : p.annual;
const note = billing === "annual" ? `Billed $${p.annual * 12}/yr` : "Billed monthly";
return (
<th key={p.id} scope="col" className="px-2 pb-4 pt-3 align-bottom sm:px-3">
<div
className={`relative flex h-full flex-col gap-3 rounded-2xl p-4 text-left ${
p.featured
? "bg-indigo-50/70 ring-1 ring-inset ring-indigo-500/40 dark:bg-indigo-500/10 dark:ring-indigo-400/30"
: ""
}`}
>
{p.featured && (
<span className="relative -mt-1 mb-0.5 inline-flex w-fit items-center gap-1 overflow-hidden rounded-full bg-indigo-600 px-2.5 py-1 text-[10px] font-bold uppercase tracking-wide text-white">
<span className="pxfm-shine pointer-events-none absolute inset-0" aria-hidden="true" />
<span className="relative">Most popular</span>
</span>
)}
<div>
<h3 className="text-sm font-semibold text-slate-900 dark:text-white">{p.name}</h3>
<p className="mt-1 text-xs font-normal leading-relaxed text-slate-500 dark:text-slate-400">
{p.tagline}
</p>
</div>
<div className="mt-auto">
<div className="flex items-baseline gap-1">
<motion.span
key={`${p.id}-${billing}`}
initial={reduce ? false : { opacity: 0, y: -6 }}
animate={reduce ? {} : { opacity: 1, y: 0 }}
transition={{ duration: 0.25, ease: "easeOut" }}
className="text-2xl font-bold tracking-tight text-slate-900 dark:text-white"
>
${price}
</motion.span>
<span className="text-xs font-medium text-slate-500 dark:text-slate-400">/mo</span>
</div>
<p className="mt-1 text-[11px] font-normal text-slate-400 dark:text-slate-500">{note}</p>
</div>
<button
type="button"
className={`inline-flex w-full items-center justify-center gap-1.5 rounded-xl px-4 py-2.5 text-sm font-semibold transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-950 ${
p.featured
? "bg-indigo-600 text-white shadow-sm hover:bg-indigo-500"
: "border border-slate-300 bg-white text-slate-800 hover:bg-slate-50 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100 dark:hover:bg-slate-800"
}`}
>
{p.cta}
<ArrowIcon />
</button>
</div>
</th>
);
})}
</tr>
</thead>
{GROUPS.map((group) => {
const isOpen = open[group.id];
const panelId = `${uid}-${group.id}`;
return (
<tbody key={group.id} id={panelId}>
<tr>
<th scope="colgroup" colSpan={4} className="p-0">
<button
type="button"
aria-expanded={isOpen}
aria-controls={panelId}
onClick={() => toggleGroup(group.id)}
className="flex w-full items-center gap-2 border-y border-slate-200 bg-slate-50 px-3 py-3 text-left text-sm font-semibold text-slate-800 transition hover:bg-slate-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-indigo-500 sm:px-4 dark:border-slate-800 dark:bg-slate-900/60 dark:text-slate-100 dark:hover:bg-slate-800/60"
>
<ChevronIcon open={isOpen} />
<span>{group.name}</span>
<span className="ml-1 rounded-full bg-slate-200 px-1.5 py-0.5 text-[10px] font-semibold text-slate-500 dark:bg-slate-700 dark:text-slate-300">
{group.rows.length}
</span>
</button>
</th>
</tr>
{isOpen &&
group.rows.map((row) => (
<tr
key={row.label}
className="border-b border-slate-100 transition-colors last:border-b-0 hover:bg-slate-50/70 dark:border-slate-800/70 dark:hover:bg-slate-800/30"
>
<th scope="row" className="px-3 py-3.5 text-left align-top text-sm font-medium text-slate-700 sm:px-4 dark:text-slate-300">
<span>{row.label}</span>
{row.hint && (
<span className="mt-0.5 block text-xs font-normal text-slate-400 dark:text-slate-500">
{row.hint}
</span>
)}
</th>
{PLANS.map((p) => {
const v = row.values[p.id];
return (
<td
key={p.id}
className={`px-2 py-3.5 text-center align-middle text-sm sm:px-3 ${
p.featured ? "bg-indigo-50/50 dark:bg-indigo-500/10" : ""
}`}
>
{typeof v === "string" ? (
<span className="font-medium text-slate-700 dark:text-slate-200">{v}</span>
) : v ? (
<span className="inline-flex items-center justify-center rounded-full bg-emerald-100 p-1 text-emerald-600 dark:bg-emerald-500/15 dark:text-emerald-400">
<CheckIcon />
<span className="sr-only">Included</span>
</span>
) : (
<span className="inline-flex items-center justify-center text-slate-300 dark:text-slate-600">
<MinusIcon />
<span className="sr-only">Not included</span>
</span>
)}
</td>
);
})}
</tr>
))}
</tbody>
);
})}
</table>
</div>
</motion.div>
<p className="mt-6 text-center text-xs text-slate-500 dark:text-slate-500">
Prices in USD, exclusive of local tax. Annual plans are billed once per year. Need something custom?{" "}
<a
href="#contact"
className="font-medium text-indigo-600 underline-offset-2 hover:underline dark:text-indigo-400"
>
Contact our team
</a>
.
</p>
</div>
</section>
);
}Dependencies
Licence
Built by Web Innoventix. Free for personal and commercial use, no attribution required.
Built by Web Innoventix
Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.
Get a free quoteSimilar components
Browse all →
Simple Pricing
OriginalA three-tier pricing section with a highlighted popular plan.

Single Plan Pricing Card
OriginalA focused one-plan pricing card with price, trial badge, feature list, call to action and a money-back trust note, ideal when you sell a single flat-rate product.

Three Tier Pricing With Monthly Annual Toggle
OriginalA three-tier pricing grid with a pure-CSS monthly and annual billing toggle that swaps every price with no JavaScript, using a native checkbox and Tailwind group-has state.

Plan Feature Comparison Table
OriginalA responsive, semantic feature comparison table across three plans with grouped rows, tick and dash indicators and screen-reader labels so visitors can weigh every feature side by side.

Staggered Glow Pricing Tiers
OriginalA three-tier pricing section whose cards reveal in a staggered spring cascade, with a rotating conic-gradient glow on the popular plan and prices that count up smoothly when you toggle monthly and annual billing.

Spotlight Cursor Pricing Cards
OriginalPricing cards that light up with a cursor-following radial spotlight, drift over floating background orbs and rise into view with a staggered spring entrance, plus a pulsing glow on the popular plan.

Highlight Follow Pricing Plans
OriginalA pricing row where an animated gradient highlight glides via shared layout animation to whichever plan you hover, a three-way billing switch slides its pill, prices recount instantly and a perks marquee scrolls beneath.

Three Tier Pricing
Originalthree-tier pricing cards

Toggle Annual Pricing
Originalpricing with monthly/annual toggle

Single Highlight Pricing
Originalsingle highlighted plan

Comparison Table Pricing
Originalpricing comparison table

Cards Gradient Pricing
Originalgradient pricing cards

