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.

Get it free

Open the demoReact · Tailwind · TypeScript

Use it in Next.js

app/pricing/page.tsx

"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

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

Paddle checkout in Next.js with a monthly/yearly pricing page