Lemon Squeezy checkout in Next.js
The free pricing switch calls a server action that creates a Lemon Squeezy checkout for the chosen plan and billing period, then sends the buyer to pay.
Use it in Next.js
"use client";
import PricingSwitch, { type Plan } from "qodesign-free/pricing-switch";
import { startCheckout } 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 product with paying users.", monthly: 29, yearly: 23, cta: "Start Pro", features: ["10 projects", "Priority support"], featured: true },
];
export default function PricingPage() {
return (
<main className="mx-auto max-w-4xl px-4 py-16">
<PricingSwitch
plans={plans}
header={<h1 className="text-3xl font-semibold">Pricing</h1>}
yearlyBadge="2 months free"
onSelect={async (plan, yearly) => {
const { url } = await startCheckout(plan.id, yearly);
window.location.assign(url);
}}
/>
</main>
);
}"use server";
import { lemonSqueezySetup, createCheckout } from "@lemonsqueezy/lemonsqueezy.js";
lemonSqueezySetup({ apiKey: process.env.LEMONSQUEEZY_API_KEY! });
// One variant per plan and billing period, from your Lemon Squeezy dashboard (test mode first).
const VARIANTS: Record<string, { monthly: number; yearly: number }> = {
starter: { monthly: 111111, yearly: 111112 },
pro: { monthly: 222221, yearly: 222222 },
};
export async function startCheckout(planId: string, yearly: boolean) {
const v = VARIANTS[planId];
if (!v) throw new Error("Unknown plan");
const { data, error } = await createCheckout(process.env.LEMONSQUEEZY_STORE_ID!, yearly ? v.yearly : v.monthly, {
checkoutData: { custom: { plan: planId } }, // add email here if the buyer is signed in
productOptions: { redirectUrl: `${process.env.NEXT_PUBLIC_SITE_URL}/welcome` },
});
if (error) throw new Error(error.message);
return { url: data.data.attributes.url };
}Good to know
- The variant ids, the store id and the API key stay on the server. The browser only gets the checkout URL back.
- The numbers on the switch come from plans. Lemon Squeezy charges what the variant says, so change both together.
- checkoutData.custom comes back in your webhooks as meta.custom_data. That is how you tie the payment to a plan or a user id.
- For the overlay instead of a full redirect, load Lemon Squeezy's lemon.js and open the same URL with LemonSqueezy.Url.Open(url).
- Give access from the subscription_created or order_created webhook, not from the redirect page. A visitor can open /welcome without paying.
- Checked against the type definitions of @lemonsqueezy/lemonsqueezy.js 4.0. We haven't run a live Lemon Squeezy checkout with it.
Install
- One command
npm i qodesign-free framer-motion @lemonsqueezy/lemonsqueezy.js - 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 pricing switch free?
Yes. MIT, from the free npm package or the shadcn registry.
Can I prefill the buyer's email?
Yes. Pass email inside checkoutData when you know who is signed in.
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.