Paddle checkout in Next.js
The free pricing switch opens Paddle Billing's overlay checkout with the right price for the plan and billing period. No server route is needed to start a checkout.
Use it in Next.js
"use client";
import { useEffect, useState } from "react";
import { initializePaddle, type Paddle } from "@paddle/paddle-js";
import PricingSwitch, { type Plan } from "qodesign-free/pricing-switch";
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 },
];
// One Paddle price per plan and billing period (Catalog > Products in the Paddle dashboard).
const PRICES: Record<string, { monthly: string; yearly: string }> = {
starter: { monthly: "pri_starter_monthly", yearly: "pri_starter_yearly" },
pro: { monthly: "pri_pro_monthly", yearly: "pri_pro_yearly" },
};
export default function PricingPage() {
const [paddle, setPaddle] = useState<Paddle>();
useEffect(() => {
initializePaddle({
environment: process.env.NODE_ENV === "production" ? "production" : "sandbox",
token: process.env.NEXT_PUBLIC_PADDLE_CLIENT_TOKEN!,
}).then(setPaddle);
}, []);
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={(plan, yearly) => {
const ids = PRICES[plan.id];
if (!paddle || !ids) return;
paddle.Checkout.open({
items: [{ priceId: yearly ? ids.yearly : ids.monthly, quantity: 1 }],
customData: { plan: plan.id }, // comes back on the transaction and in webhooks
});
}}
/>
</main>
);
}Good to know
- The client-side token (Developer tools > Authentication in Paddle) is safe in the browser. Your API key is not, and this page doesn't need it.
- Sandbox and live use different tokens and different price ids. Keep both sets in env vars and switch on the environment.
- The switch shows the numbers in plans. Paddle charges what the price says and can localise it per country. To show Paddle's own localised numbers, read them with paddle.PricePreview and pass them into plans.
- Give access from Paddle's transaction.completed or subscription.created webhook. The overlay closing is not proof of payment.
- Paddle only opens checkouts on domains you approved in its dashboard, so add localhost for sandbox testing.
- Checked against the type definitions of @paddle/paddle-js 1.6. We haven't opened a live Paddle checkout with it.
Install
- One command
npm i qodesign-free framer-motion @paddle/paddle-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 this Paddle Billing or Paddle Classic?
Paddle Billing. Classic accounts use a different script and product ids.
Can I prefill the buyer's email?
Yes. Add customer: { email } to Checkout.open when the visitor is signed in.
Is the pricing switch free?
Yes. MIT, from the free npm package or the shadcn registry.
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.