A three-plan pricing section with a billing toggle that switches every price between monthly and yearly. The middle plan is raised with a border, a soft shadow and a badge, which is where most visitors look first. It stacks into a single column on phones.
import { useState } from "react";
const plans = [
{ name: "Starter", monthly: 12, yearly: 9, blurb: "For a first project.", features: ["1 site", "Custom domain", "Basic analytics", "Email support"] },
{ name: "Growth", monthly: 29, yearly: 23, blurb: "For a growing business.", features: ["5 sites", "Custom domains", "Advanced analytics", "Priority support", "Team seats"], popular: true },
{ name: "Scale", monthly: 79, yearly: 63, blurb: "For teams and agencies.", features: ["Unlimited sites", "Client billing", "Audit log", "Dedicated manager", "99.9% uptime SLA"] },
];
export default function PricingTableWithToggle() {
const [yearly, setYearly] = useState(true);
return (
<section className="bg-white px-6 py-16 text-slate-900 sm:py-20">
<div className="mx-auto max-w-5xl">
<div className="mx-auto max-w-xl text-center">
<h2 className="text-3xl font-semibold tracking-tight sm:text-4xl">Simple pricing</h2>
<p className="mt-3 text-slate-600">Start free for 14 days. Change or cancel your plan at any time.</p>
<div className="mt-8 inline-flex items-center gap-3 rounded-full border border-slate-200 p-1 text-sm" role="group" aria-label="Billing period">
<button type="button" aria-pressed={!yearly} onClick={() => setYearly(false)} className={`rounded-full px-4 py-1.5 font-medium transition ${!yearly ? "bg-slate-900 text-white" : "text-slate-600 hover:text-slate-900"}`}>
Monthly
</button>
<button type="button" aria-pressed={yearly} onClick={() => setYearly(true)} className={`rounded-full px-4 py-1.5 font-medium transition ${yearly ? "bg-slate-900 text-white" : "text-slate-600 hover:text-slate-900"}`}>
Yearly <span className={yearly ? "text-emerald-300" : "text-emerald-600"}>-20%</span>
</button>
</div>
</div>
<div className="mt-12 grid gap-6 md:grid-cols-3">
{plans.map((p) => (
<div key={p.name} className={`relative flex flex-col rounded-2xl border p-7 ${p.popular ? "border-indigo-600 shadow-lg shadow-indigo-100" : "border-slate-200"}`}>
{p.popular && <span className="absolute -top-3 left-7 rounded-full bg-indigo-600 px-3 py-1 text-xs font-semibold text-white">Most popular</span>}
<h3 className="text-lg font-semibold">{p.name}</h3>
<p className="mt-1 text-sm text-slate-600">{p.blurb}</p>
<p className="mt-6 flex items-baseline gap-1">
<span className="text-4xl font-semibold tracking-tight">${yearly ? p.yearly : p.monthly}</span>
<span className="text-sm text-slate-500">/ month</span>
</p>
<p className="mt-1 text-xs text-slate-500">{yearly ? `Billed $${p.yearly * 12} a year` : "Billed monthly"}</p>
<ul className="mt-6 space-y-3 text-sm text-slate-700">
{p.features.map((f) => (
<li key={f} className="flex items-start gap-2.5">
<svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" className="mt-0.5 h-4 w-4 flex-none text-indigo-600">
<path fillRule="evenodd" d="M16.7 5.3a1 1 0 0 1 0 1.4l-7.5 7.5a1 1 0 0 1-1.4 0L3.3 9.7a1 1 0 1 1 1.4-1.4l3.8 3.8 6.8-6.8a1 1 0 0 1 1.4 0Z" clipRule="evenodd" />
</svg>
{f}
</li>
))}
</ul>
<a href="#" className={`mt-8 rounded-lg px-4 py-2.5 text-center text-sm font-semibold transition focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-600 focus-visible:ring-offset-2 ${p.popular ? "bg-indigo-600 text-white hover:bg-indigo-500" : "bg-slate-100 text-slate-900 hover:bg-slate-200"}`}>
Choose {p.name}
</a>
</div>
))}
</div>
</div>
</section>
);
}
Install with one command: npx shadcn@latest add https://we.inc/r/pricing-table-with-toggle.json
Each plan in the `plans` array has a `monthly` and a `yearly` number. The yearly number is the price per month when billed yearly, and the line under the price multiplies it by twelve.
Yes. Copy the HTML version for the layout and styles. The toggle needs a few lines of JavaScript to swap the prices, or you can show one period only and remove the toggle.
Yes. It uses only core utility classes that exist in both versions, with no plugins and no custom config.
Three is the most common because it gives a cheap entry point, a recommended middle and a premium option. Beyond four columns the table becomes hard to compare on small screens.
Related: Button Set: Primary, Secondary, Ghost, Icon and Loading, Split Hero with Email Capture.
More pricing or all free UI components. Start for free to have a full site built around it.
We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.