Addons Pricing
Original · freepricing with selectable add-ons
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-addons.json"use client";
import { useEffect, useId, useMemo, useState, type CSSProperties } from "react";
import {
motion,
useReducedMotion,
useMotionValue,
useTransform,
animate,
type MotionProps,
} from "motion/react";
type Plan = {
id: string;
name: string;
monthly: number;
tagline: string;
features: string[];
popular?: boolean;
};
type AddOn = {
id: string;
name: string;
desc: string;
monthly: number;
};
const PLANS: Plan[] = [
{
id: "starter",
name: "Starter",
monthly: 19,
tagline: "For solo makers shipping their first real product.",
features: [
"1 workspace",
"Up to 3 active projects",
"5 GB asset storage",
"Community support",
],
},
{
id: "growth",
name: "Growth",
monthly: 49,
tagline: "For growing teams that need room to move fast.",
features: [
"5 workspaces",
"Unlimited projects",
"100 GB asset storage",
"Automation workflows",
"Email support",
],
popular: true,
},
{
id: "scale",
name: "Scale",
monthly: 99,
tagline: "For organizations that need control at scale.",
features: [
"Unlimited workspaces",
"SSO & audit logs",
"1 TB asset storage",
"Advanced permissions",
"24/7 priority support",
],
},
];
const ADDONS: AddOn[] = [
{
id: "priority",
name: "Priority support",
desc: "Skip the queue with a 2-hour response SLA.",
monthly: 12,
},
{
id: "analytics",
name: "Advanced analytics",
desc: "Cohort, funnel, and retention dashboards.",
monthly: 18,
},
{
id: "seats",
name: "Extra seats (×5)",
desc: "Add five more collaborators to any plan.",
monthly: 25,
},
{
id: "domains",
name: "Custom domains",
desc: "Serve projects from your own branded domain.",
monthly: 8,
},
{
id: "onboarding",
name: "Onboarding & migration",
desc: "White-glove setup with a dedicated specialist.",
monthly: 40,
},
];
const ANNUAL_FACTOR = 0.8; // 20% off, two months free
function CheckIcon({
className,
style,
}: {
className?: string;
style?: CSSProperties;
}) {
return (
<svg
viewBox="0 0 20 20"
fill="none"
aria-hidden="true"
className={className}
style={style}
>
<path
d="M4 10.5 8 14.5 16 5.5"
stroke="currentColor"
strokeWidth="2.2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
function ArrowIcon({ className }: { className?: string }) {
return (
<svg
viewBox="0 0 20 20"
fill="none"
aria-hidden="true"
className={className}
>
<path
d="M4 10h11M11 5l5 5-5 5"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
function money(n: number): string {
return "$" + Math.round(n).toLocaleString("en-US");
}
export default function PricexAddons() {
const reduce = useReducedMotion();
const groupName = useId();
const [planId, setPlanId] = useState<string>("growth");
const [billing, setBilling] = useState<"monthly" | "annual">("annual");
const [addOns, setAddOns] = useState<string[]>(["analytics"]);
const plan = useMemo(
() => PLANS.find((p) => p.id === planId) ?? PLANS[0],
[planId]
);
const addOnsMonthly = useMemo(
() =>
ADDONS.filter((a) => addOns.includes(a.id)).reduce(
(sum, a) => sum + a.monthly,
0
),
[addOns]
);
const subtotal = plan.monthly + addOnsMonthly;
const factor = billing === "annual" ? ANNUAL_FACTOR : 1;
const perMonth = subtotal * factor;
const billedYearly = perMonth * 12;
const yearlySavings = subtotal * 12 * (1 - ANNUAL_FACTOR);
const price = useMotionValue(perMonth);
const priceText = useTransform(price, (v) => money(v));
useEffect(() => {
const controls = animate(price, perMonth, {
duration: reduce ? 0 : 0.45,
ease: "easeOut",
});
return () => controls.stop();
}, [perMonth, price, reduce]);
function toggleAddOn(id: string) {
setAddOns((prev) =>
prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]
);
}
const fade: MotionProps = reduce
? {}
: {
initial: { opacity: 0, y: 16 },
whileInView: { opacity: 1, y: 0 },
viewport: { once: true, margin: "-80px" },
transition: { duration: 0.5, ease: [0.22, 1, 0.36, 1] },
};
return (
<section className="relative w-full overflow-hidden bg-gradient-to-b from-slate-50 via-white to-slate-100 px-5 py-20 text-slate-900 sm:px-8 sm:py-28 dark:from-slate-950 dark:via-slate-950 dark:to-slate-900 dark:text-slate-100">
<style>{`
@keyframes pxaddon-blob {
0%, 100% { transform: translate(0, 0) scale(1); }
33% { transform: translate(28px, -22px) scale(1.08); }
66% { transform: translate(-22px, 18px) scale(0.94); }
}
@keyframes pxaddon-sheen {
0% { background-position: -180% 0; }
100% { background-position: 180% 0; }
}
@keyframes pxaddon-pop {
0% { transform: scale(0.4); opacity: 0; }
60% { transform: scale(1.15); }
100% { transform: scale(1); opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
.pxaddon-anim { animation: none !important; }
}
`}</style>
{/* Decorative background */}
<div aria-hidden="true" className="pointer-events-none absolute inset-0">
<div
className="pxaddon-anim absolute -left-24 top-8 h-72 w-72 rounded-full bg-indigo-400/25 blur-3xl dark:bg-indigo-500/20"
style={{ animation: "pxaddon-blob 20s ease-in-out infinite" }}
/>
<div
className="pxaddon-anim absolute -right-20 top-40 h-80 w-80 rounded-full bg-violet-400/20 blur-3xl dark:bg-violet-500/20"
style={{ animation: "pxaddon-blob 26s ease-in-out infinite reverse" }}
/>
<div
className="absolute inset-0 opacity-[0.35] dark:opacity-20"
style={{
backgroundImage:
"linear-gradient(to right, rgba(100,116,139,0.12) 1px, transparent 1px), linear-gradient(to bottom, rgba(100,116,139,0.12) 1px, transparent 1px)",
backgroundSize: "44px 44px",
maskImage:
"radial-gradient(ellipse 80% 60% at 50% 0%, #000 40%, transparent 100%)",
WebkitMaskImage:
"radial-gradient(ellipse 80% 60% at 50% 0%, #000 40%, transparent 100%)",
}}
/>
</div>
<div className="relative mx-auto max-w-6xl">
{/* Header */}
<motion.div {...fade} className="mx-auto max-w-2xl text-center">
<span className="inline-flex items-center gap-2 rounded-full border border-indigo-200/70 bg-white/70 px-3.5 py-1.5 text-xs font-semibold uppercase tracking-[0.14em] text-indigo-700 backdrop-blur dark:border-indigo-400/20 dark:bg-white/5 dark:text-indigo-300">
<span className="h-1.5 w-1.5 rounded-full bg-indigo-500" />
Build your plan
</span>
<h2 className="mt-6 text-balance text-4xl font-semibold tracking-tight sm:text-5xl">
Pay for the core.
<span className="bg-gradient-to-r from-indigo-600 via-violet-600 to-indigo-500 bg-clip-text text-transparent dark:from-indigo-400 dark:via-violet-400 dark:to-indigo-300">
{" "}
Add only what you need.
</span>
</h2>
<p className="mt-4 text-pretty text-base leading-relaxed text-slate-600 sm:text-lg dark:text-slate-400">
Start with a plan that fits your team, then bolt on the extras that
matter. Your total updates live — no surprises at checkout.
</p>
</motion.div>
{/* Billing toggle */}
<motion.div
{...fade}
className="mt-10 flex items-center justify-center"
>
<div
role="radiogroup"
aria-label="Billing period"
className="relative inline-flex rounded-full border border-slate-200 bg-white/80 p-1 shadow-sm backdrop-blur dark:border-white/10 dark:bg-white/5"
>
{(["monthly", "annual"] as const).map((mode) => {
const active = billing === mode;
return (
<button
key={mode}
role="radio"
aria-checked={active}
onClick={() => setBilling(mode)}
className={`relative z-10 rounded-full px-5 py-2 text-sm font-semibold capitalize transition-colors focus: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 ${
active
? "text-white"
: "text-slate-600 hover:text-slate-900 dark:text-slate-400 dark:hover:text-slate-100"
}`}
>
{active && (
<motion.span
layoutId={reduce ? undefined : `${groupName}-pill`}
transition={{ type: "spring", stiffness: 400, damping: 32 }}
className="absolute inset-0 -z-10 rounded-full bg-gradient-to-r from-indigo-600 to-violet-600 shadow-lg shadow-indigo-500/30"
/>
)}
{mode}
{mode === "annual" && (
<span
className={`ml-2 rounded-full px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-wide ${
active
? "bg-white/25 text-white"
: "bg-emerald-100 text-emerald-700 dark:bg-emerald-400/15 dark:text-emerald-300"
}`}
>
−20%
</span>
)}
</button>
);
})}
</div>
</motion.div>
{/* Main grid */}
<div className="mt-12 grid gap-8 lg:grid-cols-3">
{/* Left: plans + add-ons */}
<div className="lg:col-span-2">
{/* Plans */}
<fieldset>
<legend className="mb-4 text-sm font-semibold uppercase tracking-[0.12em] text-slate-500 dark:text-slate-400">
1 · Choose a plan
</legend>
<div className="grid gap-4 sm:grid-cols-3">
{PLANS.map((p) => {
const selected = p.id === planId;
const shown =
billing === "annual"
? p.monthly * ANNUAL_FACTOR
: p.monthly;
return (
<label
key={p.id}
className={`group relative flex cursor-pointer flex-col rounded-2xl border p-5 transition-all duration-200 ${
selected
? "border-indigo-500 bg-white shadow-xl shadow-indigo-500/10 ring-2 ring-indigo-500/40 dark:bg-slate-900"
: "border-slate-200 bg-white/60 hover:border-slate-300 hover:bg-white dark:border-white/10 dark:bg-white/[0.03] dark:hover:border-white/20"
}`}
>
<input
type="radio"
name={`${groupName}-plan`}
checked={selected}
onChange={() => setPlanId(p.id)}
className="peer sr-only"
/>
<span className="pointer-events-none absolute inset-0 rounded-2xl opacity-0 ring-2 ring-indigo-500 ring-offset-2 ring-offset-white transition peer-focus-visible:opacity-100 dark:ring-offset-slate-950" />
<div className="flex items-center justify-between">
<span className="text-base font-semibold">{p.name}</span>
<span
className={`flex h-5 w-5 items-center justify-center rounded-full border transition ${
selected
? "border-indigo-500 bg-indigo-500 text-white"
: "border-slate-300 text-transparent dark:border-slate-600"
}`}
>
<CheckIcon className="h-3.5 w-3.5" />
</span>
</div>
{p.popular && (
<span
className="pxaddon-anim mt-2 inline-flex w-fit items-center rounded-full bg-[length:200%_100%] bg-gradient-to-r from-amber-400 via-amber-200 to-amber-400 px-2 py-0.5 text-[10px] font-bold uppercase tracking-wide text-amber-900"
style={{ animation: "pxaddon-sheen 3.5s linear infinite" }}
>
Most popular
</span>
)}
<div className="mt-3 flex items-baseline gap-1">
<span className="text-2xl font-bold tracking-tight">
{money(shown)}
</span>
<span className="text-xs font-medium text-slate-500 dark:text-slate-400">
/mo
</span>
</div>
<p className="mt-2 text-xs leading-relaxed text-slate-600 dark:text-slate-400">
{p.tagline}
</p>
<ul className="mt-4 space-y-2 border-t border-slate-100 pt-4 dark:border-white/5">
{p.features.map((f) => (
<li
key={f}
className="flex items-start gap-2 text-xs text-slate-700 dark:text-slate-300"
>
<CheckIcon className="mt-0.5 h-3.5 w-3.5 shrink-0 text-indigo-500 dark:text-indigo-400" />
<span>{f}</span>
</li>
))}
</ul>
</label>
);
})}
</div>
</fieldset>
{/* Add-ons */}
<fieldset className="mt-10">
<legend className="mb-4 text-sm font-semibold uppercase tracking-[0.12em] text-slate-500 dark:text-slate-400">
2 · Select add-ons
</legend>
<div className="grid gap-3 sm:grid-cols-2">
{ADDONS.map((a) => {
const checked = addOns.includes(a.id);
const shown =
billing === "annual" ? a.monthly * ANNUAL_FACTOR : a.monthly;
return (
<label
key={a.id}
className={`group relative flex cursor-pointer items-start gap-3 rounded-xl border p-4 transition-all duration-200 ${
checked
? "border-violet-500 bg-violet-50/60 dark:border-violet-400/50 dark:bg-violet-500/10"
: "border-slate-200 bg-white/60 hover:border-slate-300 hover:bg-white dark:border-white/10 dark:bg-white/[0.03] dark:hover:border-white/20"
}`}
>
<input
type="checkbox"
checked={checked}
onChange={() => toggleAddOn(a.id)}
className="peer sr-only"
/>
<span className="pointer-events-none absolute inset-0 rounded-xl opacity-0 ring-2 ring-violet-500 ring-offset-2 ring-offset-white transition peer-focus-visible:opacity-100 dark:ring-offset-slate-950" />
<span
className={`mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded-md border transition ${
checked
? "border-violet-500 bg-violet-500 text-white"
: "border-slate-300 text-transparent dark:border-slate-600"
}`}
>
{checked && (
<CheckIcon
className="pxaddon-anim h-3.5 w-3.5"
style={{ animation: "pxaddon-pop 0.28s ease-out" }}
/>
)}
</span>
<span className="min-w-0 flex-1">
<span className="flex items-center justify-between gap-2">
<span className="text-sm font-semibold">{a.name}</span>
<span className="whitespace-nowrap text-sm font-semibold text-violet-600 dark:text-violet-300">
+{money(shown)}
<span className="text-[11px] font-medium text-slate-400">
/mo
</span>
</span>
</span>
<span className="mt-1 block text-xs leading-relaxed text-slate-600 dark:text-slate-400">
{a.desc}
</span>
</span>
</label>
);
})}
</div>
</fieldset>
</div>
{/* Right: summary */}
<motion.aside
{...fade}
className="lg:col-span-1"
aria-label="Order summary"
>
<div className="lg:sticky lg:top-8">
<div className="overflow-hidden rounded-3xl border border-slate-200 bg-white/90 shadow-2xl shadow-slate-900/5 backdrop-blur dark:border-white/10 dark:bg-slate-900/80 dark:shadow-black/40">
<div className="relative bg-gradient-to-br from-indigo-600 via-violet-600 to-indigo-700 px-6 py-7 text-white">
<div
aria-hidden="true"
className="absolute inset-0 opacity-30"
style={{
backgroundImage:
"radial-gradient(circle at 20% 20%, rgba(255,255,255,0.4) 0, transparent 45%)",
}}
/>
<div className="relative">
<p className="text-xs font-semibold uppercase tracking-[0.14em] text-indigo-100">
Your total
</p>
<div className="mt-2 flex items-end gap-1.5">
<motion.span className="text-5xl font-bold tracking-tight">
{priceText}
</motion.span>
<span className="mb-1.5 text-sm font-medium text-indigo-100">
/mo
</span>
</div>
<p className="mt-2 text-sm text-indigo-100">
{billing === "annual"
? `${money(billedYearly)} billed yearly`
: "Billed monthly · cancel anytime"}
</p>
</div>
</div>
<div className="px-6 py-5">
<dl className="space-y-2.5 text-sm">
<div className="flex items-center justify-between">
<dt className="text-slate-600 dark:text-slate-400">
{plan.name} plan
</dt>
<dd className="font-medium tabular-nums">
{money(plan.monthly)}/mo
</dd>
</div>
{ADDONS.filter((a) => addOns.includes(a.id)).map((a) => (
<div
key={a.id}
className="flex items-center justify-between"
>
<dt className="flex items-center gap-2 text-slate-600 dark:text-slate-400">
<span className="h-1 w-1 rounded-full bg-violet-400" />
{a.name}
</dt>
<dd className="font-medium tabular-nums text-violet-600 dark:text-violet-300">
+{money(a.monthly)}/mo
</dd>
</div>
))}
{addOns.length === 0 && (
<p className="text-xs italic text-slate-400">
No add-ons selected yet.
</p>
)}
<div className="!mt-4 border-t border-slate-100 pt-3 dark:border-white/10">
<div className="flex items-center justify-between">
<dt className="text-slate-600 dark:text-slate-400">
Subtotal
</dt>
<dd className="font-medium tabular-nums">
{money(subtotal)}/mo
</dd>
</div>
{billing === "annual" && (
<div className="mt-2 flex items-center justify-between">
<dt className="flex items-center gap-1.5 text-emerald-600 dark:text-emerald-400">
<CheckIcon className="h-3.5 w-3.5" />
Annual discount
</dt>
<dd className="font-semibold tabular-nums text-emerald-600 dark:text-emerald-400">
−{money(yearlySavings / 12)}/mo
</dd>
</div>
)}
</div>
</dl>
{billing === "annual" && (
<p className="mt-4 rounded-xl bg-emerald-50 px-3 py-2 text-center text-xs font-medium text-emerald-700 dark:bg-emerald-400/10 dark:text-emerald-300">
You save {money(yearlySavings)} per year
</p>
)}
<button
type="button"
className="group mt-5 flex w-full items-center justify-center gap-2 rounded-xl bg-gradient-to-r from-indigo-600 to-violet-600 px-5 py-3.5 text-sm font-semibold text-white shadow-lg shadow-indigo-500/25 transition hover:shadow-xl hover:shadow-indigo-500/40 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white active:scale-[0.99] dark:focus-visible:ring-offset-slate-900"
>
Continue to checkout
<ArrowIcon className="h-4 w-4 transition-transform group-hover:translate-x-0.5" />
</button>
<p className="mt-3 text-center text-xs text-slate-500 dark:text-slate-400">
14-day free trial · No card required
</p>
</div>
</div>
</div>
</motion.aside>
</div>
</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

