Stripe subscription pricing page

The free pricing switch sends the plan and the billing period to a server action that opens Stripe Checkout in subscription mode.

Get it freeOpen the demoReact · Tailwind · TypeScript

Use it in Next.js

app/pricing/page.tsx

"use client";
import PricingSwitch, { type Plan } from "@/components/pricing-switch/PricingSwitch";
import { startSubscription } from "./actions";

const plans: Plan[] = [
  { id: "starter", name: "Starter", description: "For side projects", monthly: 9, yearly: 7, cta: "Start Starter", features: ["1 project", "Email support"] },
  { id: "pro", name: "Pro", description: "For a real business", monthly: 29, yearly: 23, cta: "Start Pro", features: ["10 projects", "Priority support"], featured: true },
];

export default function Pricing() {
  return (
    <PricingSwitch
      plans={plans}
      onSelect={async (plan, yearly) => {
        const url = await startSubscription(plan.id, yearly);
        window.location.assign(url);
      }}
    />
  );
}

app/pricing/actions.ts

"use server";
import Stripe from "stripe";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

// one recurring Price per plan and period, made in the Stripe dashboard
const PRICES: Record<string, { monthly: string; yearly: string }> = {
  starter: { monthly: "price_starter_m", yearly: "price_starter_y" },
  pro: { monthly: "price_pro_m", yearly: "price_pro_y" },
};

export async function startSubscription(planId: string, yearly: boolean) {
  const prices = PRICES[planId];
  if (!prices) throw new Error("Unknown plan");
  const session = await stripe.checkout.sessions.create({
    mode: "subscription",
    line_items: [{ price: yearly ? prices.yearly : prices.monthly, quantity: 1 }],
    success_url: `${process.env.NEXT_PUBLIC_SITE_URL}/welcome?session_id={CHECKOUT_SESSION_ID}`,
    cancel_url: `${process.env.NEXT_PUBLIC_SITE_URL}/pricing`,
  });
  if (!session.url) throw new Error("Stripe returned no checkout URL");
  return session.url;
}

Good to know

  • The switch shows the numbers you type in plans. Stripe charges whatever the Price says. If you change one, change the other, or load the amounts from Stripe when the page builds.
  • Each plan needs two recurring Prices in Stripe, one monthly and one yearly. The yearly field in plans is the per-month figure shown when Yearly is on.
  • Free trials, coupons and taxes are Checkout session options and need no change in the component.

Install

  1. One commandnpx shadcn@latest add https://teodoravadani.github.io/qodesign-free/r/pricing-switch.json
  2. Or ask your AIPaste PROMPT.md into Cursor, Lovable or v0.

Props

plans
name, monthly, yearly, features, cta
onSelect
(plan, yearly) when a button is pressed
yearlyBadge
text on the switch, e.g. 2 months free
currency
symbol shown

How qodesign compares

All comparisons, with prices read on each library's own site.

Questions

Is the component free?

Yes. MIT, from the free registry.

Does it work with Lemon Squeezy?

Yes. Make their checkout URL in the server action instead.

Can Cursor or v0 add it for me?

Yes. The folder has a PROMPT.md. Paste it into Cursor, Lovable, v0 or Claude Code.

Does the pricing switch work in Next.js?

Yes, as a client component. Keep "use client" at the top of the file that renders it, as in the snippet above. App Router and Pages Router both work.

Stripe subscription pricing page for Next.js (free monthly/yearly switch) · qodesign