Pricing Table

Three-tier pricing with an animated monthly/yearly toggle and rolling prices.

Simple, transparent pricing

Start free. Upgrade when your team is ready.

Starter

For individuals and side projects.

$0/ month
  • 3 projects
  • Basic analytics
  • Community support
  • 1 GB storage
Most popular

Pro

For growing teams that ship weekly.

$24/ month
  • Unlimited projects
  • Advanced analytics
  • Priority support
  • 100 GB storage
  • Custom domains

Enterprise

For organizations at scale.

$79/ month
  • Everything in Pro
  • SSO & SCIM
  • Audit logs
  • Dedicated manager
  • 99.99% SLA

Installation

  1. Install the dependencies

    $ npm i motion lucide-react
  2. Copy the source into components/ui/pricing-table.tsx

    components/ui/pricing-table.tsx
    "use client";
    
    import { AnimatePresence, motion } from "motion/react";
    import { Check } from "lucide-react";
    import { useState } from "react";
    
    type Plan = {
      name: string;
      description: string;
      monthly: number;
      yearly: number;
      features: string[];
      cta: string;
      featured?: boolean;
    };
    
    const plans: Plan[] = [
      { name: "Starter", description: "For individuals and side projects.", monthly: 0, yearly: 0, cta: "Start for free", features: ["3 projects", "Basic analytics", "Community support", "1 GB storage"] },
      { name: "Pro", description: "For growing teams that ship weekly.", monthly: 29, yearly: 24, cta: "Start 14-day trial", featured: true, features: ["Unlimited projects", "Advanced analytics", "Priority support", "100 GB storage", "Custom domains"] },
      { name: "Enterprise", description: "For organizations at scale.", monthly: 99, yearly: 79, cta: "Contact sales", features: ["Everything in Pro", "SSO & SCIM", "Audit logs", "Dedicated manager", "99.99% SLA"] },
    ];
    
    export function BillingToggle({ yearly, onChange }: { yearly: boolean; onChange: (yearly: boolean) => void }) {
      return (
        <div className="relative inline-flex rounded-full border border-zinc-200 bg-zinc-50 p-1 text-sm font-medium">
          {[false, true].map((isYearly) => (
            <button
              key={String(isYearly)}
              onClick={() => onChange(isYearly)}
              className={`relative z-10 rounded-full px-5 py-2 transition-colors ${yearly === isYearly ? "text-zinc-900" : "text-zinc-500 hover:text-zinc-700"}`}
            >
              {yearly === isYearly && (
                <motion.span
                  layoutId="billing-pill"
                  className="absolute inset-0 -z-10 rounded-full bg-white shadow-sm ring-1 ring-zinc-200"
                  transition={{ type: "spring", stiffness: 400, damping: 32 }}
                />
              )}
              {isYearly ? (
                <>
                  Yearly <span className="ml-1 text-xs text-emerald-600">-20%</span>
                </>
              ) : (
                "Monthly"
              )}
            </button>
          ))}
        </div>
      );
    }
    
    export function PricingTable({ items = plans }: { items?: Plan[] }) {
      const [yearly, setYearly] = useState(true);
    
      return (
        <div className="w-full">
          <div className="flex justify-center">
            <BillingToggle yearly={yearly} onChange={setYearly} />
          </div>
          <div className="mt-10 grid gap-5 lg:grid-cols-3">
            {items.map((plan, i) => {
              const price = yearly ? plan.yearly : plan.monthly;
              return (
                <motion.div
                  key={plan.name}
                  initial={{ opacity: 0, y: 24 }}
                  whileInView={{ opacity: 1, y: 0 }}
                  viewport={{ once: true, amount: 0.3 }}
                  transition={{ duration: 0.6, delay: i * 0.1, ease: [0.22, 1, 0.36, 1] }}
                  className={`relative flex flex-col rounded-3xl p-7 ${
                    plan.featured
                      ? "bg-zinc-900 text-white shadow-2xl shadow-zinc-900/25 lg:-my-3 lg:py-10"
                      : "border border-zinc-200 bg-white text-zinc-900"
                  }`}
                >
                  {plan.featured && (
                    <span className="absolute right-6 top-6 rounded-full bg-white/10 px-2.5 py-1 text-xs font-medium text-white ring-1 ring-white/20">
                      Most popular
                    </span>
                  )}
                  <h3 className="text-lg font-semibold">{plan.name}</h3>
                  <p className={`mt-1 text-sm ${plan.featured ? "text-zinc-400" : "text-zinc-500"}`}>{plan.description}</p>
                  <div className="mt-6 flex items-baseline gap-1">
                    <span className="text-5xl font-semibold tracking-tight">$</span>
                    <span className="relative inline-flex h-[1.1em] overflow-hidden text-5xl font-semibold tracking-tight">
                      <AnimatePresence mode="popLayout" initial={false}>
                        <motion.span
                          key={price}
                          initial={{ y: "100%", opacity: 0 }}
                          animate={{ y: 0, opacity: 1 }}
                          exit={{ y: "-100%", opacity: 0 }}
                          transition={{ type: "spring", stiffness: 300, damping: 28 }}
                        >
                          {price}
                        </motion.span>
                      </AnimatePresence>
                    </span>
                    <span className={`ml-1 text-sm ${plan.featured ? "text-zinc-400" : "text-zinc-500"}`}>/ month</span>
                  </div>
                  <ul className="mt-7 flex-1 space-y-3 text-sm">
                    {plan.features.map((f) => (
                      <li key={f} className="flex items-center gap-2.5">
                        <Check className={`size-4 shrink-0 ${plan.featured ? "text-blue-400" : "text-blue-600"}`} />
                        <span className={plan.featured ? "text-zinc-300" : "text-zinc-600"}>{f}</span>
                      </li>
                    ))}
                  </ul>
                  <button
                    className={`mt-8 rounded-full py-3 text-sm font-medium transition-all hover:-translate-y-0.5 ${
                      plan.featured ? "bg-white text-zinc-900 hover:bg-zinc-100" : "bg-zinc-900 text-white hover:bg-zinc-800"
                    }`}
                  >
                    {plan.cta}
                  </button>
                </motion.div>
              );
            })}
          </div>
        </div>
      );
    }
    
    export default function Demo() {
      return (
        <section className="w-full max-w-5xl py-6">
          <div className="mb-10 text-center">
            <h2 className="text-4xl font-semibold tracking-tight text-zinc-900">Simple, transparent pricing</h2>
            <p className="mt-3 text-zinc-500">Start free. Upgrade when your team is ready.</p>
          </div>
          <PricingTable />
        </section>
      );
    }
    
  3. Use it

    The file exports the component and a Demo showing typical usage. Import whichever you need.