← PricingOpen in v0
Pricing 02 — Billing Toggle
Joined three-column pricing panel with a monthly/yearly pill toggle, animated prices and a save-20% badge.
$ npx shadcn@latest add @ludusvibe/pricing-02Code
Installed to components/sections/pricing-02.tsx — it's yours to edit.
"use client"
import * as React from "react"
import { motion } from "motion/react"
import { Check } from "lucide-react"
import { Badge } from "@/components/ui/badge"
import { buttonVariants } from "@/components/ui/button"
import { Reveal } from "@/components/ludus/reveal"
import { scaleIn } from "@/lib/motion"
import { cn } from "@/lib/utils"
type Cta = { label: string; href: string }
export type Pricing02Tier = {
name: string
description: string
/** Price per month when billed monthly. `null` renders `customPriceLabel`. */
monthlyPrice: number | null
/** Price per month when billed yearly. `null` renders `customPriceLabel`. */
yearlyPrice: number | null
features: string[]
cta: Cta
/** Visually emphasizes this tier with a badge, tint and accent bar. */
highlighted?: boolean
}
export type Pricing02Props = {
eyebrow?: string
title?: React.ReactNode
description?: string
tiers?: Pricing02Tier[]
monthlyLabel?: string
yearlyLabel?: string
/** Small badge rendered inside the yearly toggle button. */
yearlyBadge?: string
/** Currency symbol prefixed to numeric prices. */
currency?: string
/** Suffix rendered after numeric prices, e.g. "/month". */
periodLabel?: string
/** Price text for tiers with `monthlyPrice: null`. */
customPriceLabel?: string
customPriceNote?: string
billedMonthlyNote?: string
billedYearlyNote?: string
/** Badge label shown on the highlighted tier. */
highlightLabel?: string
/** Heading element — use "h1" when this is the page's main heading. */
as?: "h1" | "h2"
className?: string
}
const defaultTiers: Pricing02Tier[] = [
{
name: "Starter",
description: "For solo builders shipping their first product.",
monthlyPrice: 19,
yearlyPrice: 15,
features: [
"Up to 3 projects",
"10k monthly page views",
"Core analytics dashboard",
"Community support",
],
cta: { label: "Start with Starter", href: "#" },
},
{
name: "Pro",
description: "For growing teams that need room to move fast.",
monthlyPrice: 49,
yearlyPrice: 39,
features: [
"Unlimited projects",
"250k monthly page views",
"Advanced funnels & cohorts",
"A/B testing built in",
"5 team seats",
"Priority support",
],
cta: { label: "Get started with Pro", href: "#" },
highlighted: true,
},
{
name: "Enterprise",
description: "For organizations with security and scale requirements.",
monthlyPrice: null,
yearlyPrice: null,
features: [
"Everything in Pro",
"SSO / SAML & audit logs",
"99.99% uptime SLA",
"Unlimited team seats",
"Dedicated success manager",
],
cta: { label: "Talk to sales", href: "#" },
},
]
export function Pricing02({
eyebrow = "Pricing",
title = "Pay monthly, or save with yearly",
description = "Every plan starts with a 14-day free trial — full access, no credit card. Switch billing at any time.",
tiers = defaultTiers,
monthlyLabel = "Monthly",
yearlyLabel = "Yearly",
yearlyBadge = "Save 20%",
currency = "$",
periodLabel = "/month",
customPriceLabel = "Custom",
customPriceNote = "Tailored to your team",
billedMonthlyNote = "Billed monthly",
billedYearlyNote = "Billed annually",
highlightLabel = "Most popular",
as: Heading = "h2",
className,
}: Pricing02Props) {
const [billing, setBilling] = React.useState<"monthly" | "yearly">("monthly")
return (
<section className={cn("bg-background px-6 py-24 md:py-32", className)}>
<div className="mx-auto max-w-6xl">
<div className="mx-auto max-w-2xl text-center">
<Reveal>
<p className="text-sm font-medium tracking-widest text-primary uppercase">
{eyebrow}
</p>
</Reveal>
<Reveal delay={0.1}>
<Heading className="mt-4 text-balance font-heading text-4xl font-semibold tracking-tight md:text-5xl">
{title}
</Heading>
</Reveal>
<Reveal delay={0.2}>
<p className="mt-5 text-pretty text-muted-foreground md:text-lg">
{description}
</p>
</Reveal>
</div>
<Reveal delay={0.3} className="mt-10 flex justify-center">
<div className="inline-flex items-center rounded-full border bg-muted p-1">
<button
type="button"
onClick={() => setBilling("monthly")}
aria-pressed={billing === "monthly"}
className={cn(
"rounded-full px-4 py-1.5 text-sm font-medium transition-colors outline-none focus-visible:ring-2 focus-visible:ring-ring/50",
billing === "monthly"
? "bg-background text-foreground shadow-sm"
: "text-muted-foreground hover:text-foreground"
)}
>
{monthlyLabel}
</button>
<button
type="button"
onClick={() => setBilling("yearly")}
aria-pressed={billing === "yearly"}
className={cn(
"flex items-center gap-2 rounded-full px-4 py-1.5 text-sm font-medium transition-colors outline-none focus-visible:ring-2 focus-visible:ring-ring/50",
billing === "yearly"
? "bg-background text-foreground shadow-sm"
: "text-muted-foreground hover:text-foreground"
)}
>
{yearlyLabel}
<span className="rounded-full bg-primary/10 px-2 py-0.5 text-xs font-medium text-primary">
{yearlyBadge}
</span>
</button>
</div>
</Reveal>
<Reveal
variants={scaleIn}
delay={0.15}
className="mt-12 md:mt-14"
>
<div className="grid overflow-hidden rounded-3xl border bg-card max-md:divide-y md:grid-cols-3 md:divide-x">
{tiers.map((tier) => {
const price =
billing === "monthly" ? tier.monthlyPrice : tier.yearlyPrice
const note =
price === null
? customPriceNote
: billing === "yearly"
? billedYearlyNote
: billedMonthlyNote
return (
<div
key={tier.name}
className={cn(
"relative flex flex-col p-8",
tier.highlighted && "bg-primary/5"
)}
>
{tier.highlighted && (
<div
aria-hidden
className="absolute inset-x-0 top-0 h-1 bg-gradient-to-r from-primary/40 via-primary to-primary/40"
/>
)}
<div className="flex items-center gap-2.5">
<h3 className="font-heading text-lg font-semibold tracking-tight">
{tier.name}
</h3>
{tier.highlighted && (
<Badge className="rounded-full">{highlightLabel}</Badge>
)}
</div>
<p className="mt-2 text-sm text-pretty text-muted-foreground">
{tier.description}
</p>
<div className="mt-6 flex items-baseline gap-1.5">
<motion.span
key={`${tier.name}-${billing}`}
variants={scaleIn}
initial="hidden"
animate="visible"
className="inline-block font-heading text-5xl font-semibold tracking-tighter"
>
{price === null
? customPriceLabel
: `${currency}${price}`}
</motion.span>
{price !== null && (
<span className="text-sm text-muted-foreground">
{periodLabel}
</span>
)}
</div>
<p className="mt-1.5 text-xs text-muted-foreground">{note}</p>
<ul className="mt-8 mb-8 flex flex-col gap-3">
{tier.features.map((feature) => (
<li key={feature} className="flex items-start gap-3">
<span className="mt-0.5 flex size-5 shrink-0 items-center justify-center rounded-full bg-primary/10">
<Check className="size-3 text-primary" />
</span>
<span className="text-sm text-muted-foreground">
{feature}
</span>
</li>
))}
</ul>
<a
href={tier.cta.href}
className={cn(
buttonVariants({
size: "lg",
variant: tier.highlighted ? "default" : "outline",
}),
"mt-auto w-full"
)}
>
{tier.cta.label}
</a>
</div>
)
})}
</div>
</Reveal>
</div>
</section>
)
}