👋You’re viewing the live demo of NextFlow Kit — the SaaS starter template. Everything here is included out of the box.
Nov 9, 2025

How to Build a High-Converting Pricing Page in Next.js

Design, copy, and code patterns that make pricing pages convert. With toggles, tiers, FAQs, social proof, and Stripe Checkout.

#nextjs#stripe#pricing#conversion#ux

A great pricing page can double your conversions without touching your product. In this guide, you’ll build a high-converting pricing page in Next.js with the right UX patterns (monthly/yearly toggle, comparison table, FAQs, social proof) and wire it to Stripe Checkout so people can buy in one click. ⚡

🎯 Principles of pricing pages that convert

  • One primary CTA per tier (no decision fatigue).
  • Clear value props per plan (3–5 bullets).
  • Monthly ↔ Yearly toggle with visible savings (e.g., “Save 20%”).
  • Risk reducers: refund policy, cancel anytime, trial messaging.
  • Proof: logos, testimonials, usage stats.
  • Objection handling: FAQ close to the CTAs.

For inspiration and best practices:

  • Pricing UX teardowns: https://www.lennysnewsletter.com/p/pricing-pages
  • Stripe pricing model guides: https://stripe.com/pricing#pricing-models

đź§© Page layout (Next.js component)

A clean starter layout with three tiers and a monthly/yearly toggle.

// app/pricing/page.tsx
"use client";
import { useState } from "react";
import Link from "next/link";

type Plan = {
  name: string;
  monthly: number; // cents
  yearly: number;  // cents
  description: string;
  features: string[];
  priceIdMonthly?: string; // Stripe price IDs if you have them
  priceIdYearly?: string;
  highlight?: boolean;
};

const plans: Plan[] = [
  {
    name: "Starter",
    monthly: 990,
    yearly: 9900,
    description: "Launch your first project in minutes.",
    features: ["1 site", "Email support", "Basic analytics"],
    priceIdMonthly: "price_monthly_starter",
    priceIdYearly: "price_yearly_starter",
  },
  {
    name: "Pro",
    monthly: 1990,
    yearly: 19900,
    description: "Serious features for growing products.",
    features: ["3 sites", "Priority support", "Advanced analytics"],
    priceIdMonthly: "price_monthly_pro",
    priceIdYearly: "price_yearly_pro",
    highlight: true,
  },
  {
    name: "Business",
    monthly: 4990,
    yearly: 49900,
    description: "Everything teams need to scale.",
    features: ["10 sites", "SLA support", "Team management"],
    priceIdMonthly: "price_monthly_business",
    priceIdYearly: "price_yearly_business",
  },
];

export default function PricingPage() {
  const [yearly, setYearly] = useState(true);

  return (
    <div className="mx-auto max-w-6xl px-6 py-16">
      <div className="text-center mb-8">
        <p className="inline-flex items-center gap-2 rounded-full border px-3 py-1 text-sm">
          Flexible pricing <span aria-hidden>đź’¸</span>
        </p>
        <p className="mt-4 text-xl text-neutral-600">
          Start free, upgrade when you’re ready. Cancel anytime.
        </p>
      </div>

      {/* Toggle */}
      <div className="mb-10 flex items-center justify-center gap-3">
        <button
          onClick={() => setYearly(false)}
          className={`rounded-md px-3 py-1 text-sm ${!yearly ? "bg-black text-white" : "bg-neutral-100"}`}
          aria-pressed={!yearly}
        >
          Monthly
        </button>
        <button
          onClick={() => setYearly(true)}
          className={`rounded-md px-3 py-1 text-sm ${yearly ? "bg-black text-white" : "bg-neutral-100"}`}
          aria-pressed={yearly}
        >
          Yearly <span className="ml-1 text-emerald-600">Save 20%</span>
        </button>
      </div>

      {/* Plans */}
      <div className="grid gap-6 md:grid-cols-3">
        {plans.map((plan) => {
          const amount = yearly ? plan.yearly : plan.monthly;
          const display = (amount / 100).toLocaleString(undefined, {
            style: "currency",
            currency: "USD",
            maximumFractionDigits: 0,
          });

          return (
            <div
              key={plan.name}
              className={`rounded-2xl border p-6 ${plan.highlight ? "border-black shadow-lg" : "border-neutral-200"}`}
            >
              <h2 className="text-lg font-semibold">{plan.name}</h2>
              <p className="mt-1 text-sm text-neutral-600">{plan.description}</p>

              <div className="mt-6">
                <div className="flex items-end gap-2">
                  <span className="text-4xl font-bold">{display}</span>
                  <span className="text-neutral-500">{yearly ? "/year" : "/month"}</span>
                </div>
              </div>

              <ul className="mt-6 space-y-2 text-sm">
                {plan.features.map((f) => (
                  <li key={f} className="flex items-start gap-2">
                    <span aria-hidden>âś…</span>
                    <span>{f}</span>
                  </li>
                ))}
              </ul>

              <form
                className="mt-6"
                action="/api/create-checkout-session"
                method="POST"
              >
                <input
                  type="hidden"
                  name="priceId"
                  value={yearly ? plan.priceIdYearly : plan.priceIdMonthly}
                />
                <button
                  className={`w-full rounded-md px-4 py-2 text-sm font-medium ${plan.highlight ? "bg-black text-white" : "bg-neutral-900 text-white"}`}
                >
                  Get {plan.name}
                </button>
              </form>

              <p className="mt-3 text-xs text-neutral-500">
                14-day refund • Cancel anytime • No hidden fees
              </p>
            </div>
          );
        })}
      </div>

      {/* Proof & FAQ */}
      <div className="mt-16 grid gap-12 md:grid-cols-2">
        <section aria-labelledby="proof">
          <h3 id="proof" className="text-base font-semibold">Trusted by builders</h3>
          <p className="mt-2 text-sm text-neutral-600">
            Join startups and indie makers launching products in hours, not weeks. <span aria-hidden>🚀</span>
          </p>
          <ul className="mt-4 grid grid-cols-2 gap-3 text-sm text-neutral-700">
            <li>• 1,000+ deployments</li>
            <li>• 99.9% uptime</li>
            <li>• <Link className="underline" href="/blog">Read success stories</Link></li>
            <li>• <a className="underline" href="https://vercel.com/templates/next.js" target="_blank" rel="noreferrer">Vercel templates</a></li>
          </ul>
        </section>

        <section aria-labelledby="faq">
          <h3 id="faq" className="text-base font-semibold">FAQ</h3>
          <details className="mt-3 rounded-md border p-3">
            <summary className="cursor-pointer select-none">Can I switch plans later?</summary>
            <p className="mt-2 text-sm text-neutral-600">Yes, upgrades or downgrades take effect on your next billing cycle.</p>
          </details>
          <details className="mt-3 rounded-md border p-3">
            <summary className="cursor-pointer select-none">Do you offer refunds?</summary>
            <p className="mt-2 text-sm text-neutral-600">We offer a 14-day refund window. Just reach out if something’s not right.</p>
          </details>
          <details className="mt-3 rounded-md border p-3">
            <summary className="cursor-pointer select-none">Is my payment secure?</summary>
            <p className="mt-2 text-sm text-neutral-600">
              All payments are handled by <a className="underline" href="https://stripe.com/docs/security/stripe" target="_blank" rel="noreferrer">Stripe</a> PCI-compliant and industry-standard security.
            </p>
          </details>
        </section>
      </div>
    </div>
  );
}

This client page renders the toggle, tiers, and a native form per tier that posts a priceId to your server route.


🔌 Hook up Stripe Checkout (server route)

A minimal server route that reads the priceId from the form post and creates a Checkout Session.

// app/api/create-checkout-session/route.ts
import { NextRequest, NextResponse } from "next/server";
import Stripe from "stripe";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: "2025-04-30.basil",
});

export async function POST(req: NextRequest) {
  const form = await req.formData();
  const priceId = form.get("priceId") as string | null;

  if (!priceId) {
    return NextResponse.json({ error: "Missing priceId" }, { status: 400 });
  }

  try {
    const session = await stripe.checkout.sessions.create({
      mode: "subscription", // or "payment" for one-time
      line_items: [{ price: priceId, quantity: 1 }],
      success_url: `${process.env.NEXT_PUBLIC_APP_URL}/success`,
      cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/pricing`,
      allow_promotion_codes: true,
    });

    return NextResponse.redirect(session.url!, { status: 303 });
  } catch (e) {
    console.error(e);
    return NextResponse.json({ error: "Failed to create checkout session" }, { status: 500 });
  }
}

If you sell one-time licenses, switch mode: "payment" and use one-time Price IDs.

Stripe references:

  • Checkout Sessions: https://stripe.com/docs/api/checkout/sessions/create
  • Subscription vs one-time pricing: https://stripe.com/docs/billing/prices-guide

đź§  Copy & UX tips that lift conversions

  • Emphasize outcomes (“Launch in hours”) not features.
  • Use microcopy under the button: “14-day refund • Cancel anytime”.
  • Show the most popular plan (visual highlight).
  • Keep three tiers max to reduce analysis paralysis.
  • Add FAQs near the CTAs to resolve objections immediately.

Great UX primers:

  • NNg clarity first: https://www.nngroup.com/articles/ux-guidelines/
  • Copy tips for pricing: https://www.appcues.com/blog/pricing-page-examples

✅ Final checklist ⚙️

  • [ ] Stripe price IDs wired to each tier
  • [ ] Toggle works and shows real savings
  • [ ] One primary button per plan (no dead ends)
  • [ ] Refund/cancel policy stated near CTAs
  • [ ] Social proof and FAQs present
  • [ ] Success & cancel routes exist and tested
  • [ ] Metadata set for /pricing (title, description, canonical, OG)

With this setup, your pricing page is fast, clear, and persuasive. It connects directly to Stripe so visitors can become customers in one click. đź›’