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.

Get it free

Open the demoReact · Tailwind · TypeScript

Use it in Next.js

app/pricing/page.tsx

"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>
  );
}

app/pricing/actions.ts

"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

  1. One commandnpm i qodesign-free framer-motion @lemonsqueezy/lemonsqueezy.js
  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 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.

Lemon Squeezy checkout in Next.js with a monthly/yearly pricing