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.
Use it in Next.js
"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);
}}
/>
);
}"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
- One command
npx shadcn@latest add https://teodoravadani.github.io/qodesign-free/r/pricing-switch.json - 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
- qodesign vs 21st.dev21st.dev is best for: browsing a huge marketplace and copying many components a month.
- qodesign vs Magic UIMagic UI is best for: landing pages built with shadcn/ui.
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.