Two-Plan Pricing with Comparison Lists

A pricing section for products with exactly two plans. Both plans sit in one joined card: a white half and a dark teal half for the plan you recommend. Under each price is the same list of rows, with a limit, a tick or a dash on the right, so a visitor can read across and see what the higher plan adds. On phones the halves stack and each keeps its full list.

Code

const plans = [
  { name: "Solo", price: 19, blurb: "For freelancers who send a handful of invoices each month.", cta: "Start with Solo", featured: false },
  { name: "Studio", price: 49, blurb: "For small teams that bill clients every week and want it automated.", cta: "Start with Studio", featured: true },
];

// One row per feature. `values` follows the order of `plans`: text, or true / false.
const rows: { label: string; values: (string | boolean)[] }[] = [
  { label: "Invoices per month", values: ["20", "Unlimited"] },
  { label: "Team seats", values: ["1", "5 included"] },
  { label: "Saved clients", values: ["10", "Unlimited"] },
  { label: "Online card payments", values: [true, true] },
  { label: "Recurring invoices", values: [true, true] },
  { label: "Automatic payment reminders", values: [false, true] },
  { label: "Your logo and colours", values: [false, true] },
  { label: "Accountant export", values: [false, true] },
];

export default function TwoPlanPricingWithComparisonLists() {
  return (
    <section className="bg-stone-50 px-4 py-16 text-stone-900 sm:px-6 sm:py-20">
      <div className="mx-auto max-w-4xl">
        <div className="max-w-xl">
          <p className="text-sm font-semibold text-teal-700">Pricing</p>
          <h2 className="mt-2 text-3xl font-semibold tracking-tight sm:text-4xl">Two plans. Pick by how often you bill.</h2>
          <p className="mt-3 text-stone-600">Both include a 14-day trial. The lists below use the same rows, so you can read straight across.</p>
        </div>

        <div className="mt-10 grid overflow-hidden rounded-3xl ring-1 ring-stone-200 md:grid-cols-2">
          {plans.map((plan, i) => {
            const dark = plan.featured;
            return (
              <div key={plan.name} className={`flex flex-col p-7 sm:p-9 ${dark ? "bg-teal-900 text-white" : "bg-white"}`}>
                <div className="flex items-center justify-between gap-3">
                  <h3 className="text-lg font-semibold">{plan.name}</h3>
                  {dark && <span className="rounded-full bg-teal-700 px-3 py-1 text-xs font-semibold text-white">Most teams pick this</span>}
                </div>
                <p className="mt-5 flex items-baseline gap-2">
                  <span className="text-5xl font-semibold tracking-tight">${plan.price}</span>
                  <span className={`text-sm ${dark ? "text-teal-100" : "text-stone-600"}`}>per month</span>
                </p>
                <p className={`mt-3 text-sm leading-relaxed ${dark ? "text-teal-100" : "text-stone-600"}`}>{plan.blurb}</p>
                <a
                  href="#"
                  className={`mt-7 inline-flex h-12 items-center justify-center rounded-full px-6 text-sm font-semibold transition focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 ${
                    dark
                      ? "bg-white text-teal-900 hover:bg-teal-50 focus-visible:ring-white focus-visible:ring-offset-teal-900"
                      : "border border-stone-300 text-stone-900 hover:bg-stone-100 focus-visible:ring-teal-700"
                  }`}
                >
                  {plan.cta}
                </a>

                <ul className={`mt-8 divide-y text-sm ${dark ? "divide-teal-800" : "divide-stone-200"}`}>
                  {rows.map((row) => {
                    const value = row.values[i];
                    return (
                      <li key={row.label} className="flex items-center justify-between gap-4 py-3">
                        <span className={dark ? "text-teal-50" : "text-stone-700"}>{row.label}</span>
                        {typeof value === "string" ? (
                          <span className="flex-none font-semibold">{value}</span>
                        ) : value ? (
                          <span className="flex-none">
                            <svg viewBox="0 0 20 20" fill="none" aria-hidden="true" className={`h-5 w-5 ${dark ? "text-teal-300" : "text-teal-700"}`}>
                              <path d="M4.5 10.5l3.5 3.5 7.5-8" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
                            </svg>
                            <span className="sr-only">Included</span>
                          </span>
                        ) : (
                          <span className="flex-none">
                            <svg viewBox="0 0 20 20" fill="none" aria-hidden="true" className="h-5 w-5 text-stone-300">
                              <path d="M6 10h8" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
                            </svg>
                            <span className="sr-only">Not included</span>
                          </span>
                        )}
                      </li>
                    );
                  })}
                </ul>
              </div>
            );
          })}
        </div>

        <p className="mt-6 text-sm text-stone-600">Prices in US dollars. Move between plans whenever you like and we prorate the difference.</p>
      </div>
    </section>
  );
}

Install with one command: npx shadcn@latest add https://we.inc/r/two-plan-pricing-with-comparison-lists.json

When to use it

How to customize it

Accessibility

Questions

Is two plans enough for a pricing page?

Yes, when your customers fall into two clear groups, such as one person and a team. Two plans are quicker to choose between than three, and you can add a third later once you know what people ask for.

How do I show a feature that one plan does not have?

Set that plan's entry in the row's `values` to `false`. It shows a grey dash and reads as "Not included" to screen readers. Keeping the row on both plans is what makes the two lists comparable.

Can I add a monthly and yearly switch?

Yes. Add a `yearly` price to each plan, keep a `useState` boolean for the period, and show one price or the other. The sample pricing table with a toggle in this gallery shows the pattern.

Why is the same list repeated under each plan?

Because on a phone the plans stack, and a list that only says "everything in Solo, plus" forces the reader to scroll back up. Repeating the rows keeps each plan complete by itself.

Does this work with Tailwind CSS v3 and v4?

Yes. It uses only core utility classes, with no plugins and no custom theme values.

Related: Pricing Table with Monthly and Yearly Toggle, Pricing Comparison Table with Plans as Columns, Single Price Card with Included List and Guarantee, Button Set: Primary, Secondary, Ghost, Icon and Loading.

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