Pricing Table with Monthly and Yearly Toggle

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.

Code

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

When to use it

How to customize it

Accessibility

Questions

How do I change the prices?

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.

Can I use this without React?

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.

Does it work with Tailwind CSS v3 and v4?

Yes. It uses only core utility classes that exist in both versions, with no plugins and no custom config.

How many plans should a pricing table have?

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.

More in Pricing

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.

Product

Who It's For

Features

Resources

Company

View Sitemap