Simple, transparent pricing
Features
- ✅ Clean design - Subtle colors and professional styling
- ✅ Smooth animations - Elegant transitions between states
- ✅ Savings badge - Shows discount when yearly is selected
- ✅ Customizable labels - Change text for monthly/yearly
- ✅ Callback support - Get notified on period change
- ✅ Accessible - Keyboard navigation and ARIA labels
- ✅ TypeScript support - Full type safety
- ✅ Responsive - Works on all screen sizes
Installation
Copy and paste the following code into your project.
"use client";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { ArrowRight, Check, Clock, X } from "lucide-react";
import { useState } from "react";
interface PricingFeature {
text: string;
included: boolean;
comingSoon?: boolean;
}
interface PricingTier {
name: string;
description: string;
price: {
monthly: number;
yearly: number;
};
discount?: {
percentage: number;
label?: string;
};
badge?: string;
popular?: boolean;
features: PricingFeature[];
cta: {
text: string;
href?: string;
onClick?: () => void;
};
}
interface PricingToggleProps {
badge?: string;
headline: string;
description?: string;
tiers: PricingTier[];
defaultPeriod?: "monthly" | "yearly";
monthlyLabel?: string;
yearlyLabel?: string;
savingsPercentage?: number;
}
export function PricingToggle({
badge,
headline,
description,
tiers,
defaultPeriod = "monthly",
monthlyLabel = "Monthly",
yearlyLabel = "Yearly",
savingsPercentage = 20,
}: PricingToggleProps) {
const [period, setPeriod] = useState<"monthly" | "yearly">(defaultPeriod);
return (
<section className="relative overflow-hidden py-24 sm:py-32">
<div className="absolute inset-0 -z-10 bg-gradient-to-br from-background via-muted/30 to-background" />
<div className="absolute inset-0 -z-10 bg-[radial-gradient(circle_at_30%_20%,rgba(120,119,198,0.05),transparent_50%),radial-gradient(circle_at_70%_80%,rgba(120,119,198,0.05),transparent_50%)]" />
<div className="absolute inset-0 -z-10 bg-[linear-gradient(to_right,#80808012_1px,transparent_1px),linear-gradient(to_bottom,#80808012_1px,transparent_1px)] bg-[size:24px_24px]" />
<div className="container mx-auto px-6">
<div className="mx-auto mb-12 max-w-3xl text-center">
{badge && (
<div className="mb-6 inline-flex animate-in fade-in slide-in-from-bottom-3 duration-700">
<Badge
variant="outline"
className="border-border/40 bg-muted/50 px-3 py-1 text-xs font-medium text-muted-foreground"
>
{badge}
</Badge>
</div>
)}
<h2 className="mb-6 animate-in fade-in slide-in-from-bottom-4 bg-gradient-to-br from-foreground to-foreground/70 bg-clip-text text-4xl font-bold tracking-tight text-transparent duration-1000 sm:text-5xl md:text-6xl">
{headline}
</h2>
{description && (
<p className="animate-in fade-in slide-in-from-bottom-5 mb-8 text-lg leading-relaxed text-muted-foreground duration-1000 sm:text-xl">
{description}
</p>
)}
<div className="animate-in fade-in slide-in-from-bottom-6 flex items-center justify-center gap-3 duration-1000">
<button
onClick={() => setPeriod("monthly")}
className={`relative rounded-lg px-6 py-2.5 text-sm font-medium transition-all duration-300 ${
period === "monthly"
? "text-foreground"
: "text-muted-foreground hover:text-foreground"
}`}
>
{period === "monthly" && (
<div className="absolute inset-0 rounded-lg bg-gradient-to-br from-primary/10 to-primary/5 ring-1 ring-primary/20" />
)}
<span className="relative">{monthlyLabel}</span>
</button>
<button
onClick={() =>
setPeriod(period === "monthly" ? "yearly" : "monthly")
}
className="relative h-8 w-14 rounded-full bg-muted transition-colors duration-300 hover:bg-muted/80"
aria-label="Toggle billing period"
>
<div
className={`absolute top-1 h-6 w-6 rounded-full bg-gradient-to-br from-primary to-primary/80 shadow-lg transition-all duration-300 ${
period === "yearly" ? "left-7" : "left-1"
}`}
/>
</button>
<button
onClick={() => setPeriod("yearly")}
className={`relative rounded-lg px-6 py-2.5 text-sm font-medium transition-all duration-300 ${
period === "yearly"
? "text-foreground"
: "text-muted-foreground hover:text-foreground"
}`}
>
{period === "yearly" && (
<div className="absolute inset-0 rounded-lg bg-gradient-to-br from-primary/10 to-primary/5 ring-1 ring-primary/20" />
)}
<span className="relative">{yearlyLabel}</span>
</button>
{period === "yearly" && (
<div className="animate-in fade-in slide-in-from-left-2 rounded-md bg-green-500/10 px-2.5 py-1 ring-1 ring-green-500/20 duration-300">
<p className="text-xs font-semibold text-green-600 dark:text-green-400">
Save {savingsPercentage}%
</p>
</div>
)}
</div>
</div>
<div className="mx-auto grid max-w-7xl gap-8 overflow-visible lg:grid-cols-3">
{tiers.map((tier, index) => (
<div
key={index}
className="group relative animate-in fade-in slide-in-from-bottom-6 duration-1000"
style={{ animationDelay: `${index * 100 + 200}ms` }}
>
{tier.popular && (
<div className="absolute -top-3 left-1/2 z-10 -translate-x-1/2">
<div className="relative">
<div className="absolute inset-0 rounded-full bg-primary/20 blur-md" />
<Badge className="relative whitespace-nowrap bg-gradient-to-r from-primary via-primary to-primary/90 px-4 py-1.5 text-[11px] font-semibold uppercase tracking-wide text-primary-foreground shadow-lg">
Most Popular
</Badge>
</div>
</div>
)}
<Card
className={`relative h-full transition-all duration-500 ${
tier.popular
? "mt-3 scale-[1.02] border-primary/40 bg-gradient-to-b from-card to-card/50 shadow-2xl shadow-primary/10 ring-1 ring-primary/10 lg:scale-105"
: "mt-3 border-border/40 bg-card/50 backdrop-blur-sm hover:border-border/60 hover:shadow-xl"
}`}
>
{tier.popular && (
<>
<div className="absolute inset-0 -z-10 bg-gradient-to-br from-primary/[0.03] via-transparent to-primary/[0.03]" />
<div className="absolute -right-20 -top-20 h-40 w-40 rounded-full bg-primary/5 blur-3xl" />
<div className="absolute -bottom-20 -left-20 h-40 w-40 rounded-full bg-primary/5 blur-3xl" />
</>
)}
<CardHeader className="space-y-6 pb-8 pt-10">
<div className="flex items-start justify-between gap-2">
<h3 className="text-2xl font-bold tracking-tight">
{tier.name}
</h3>
<div className="flex flex-col items-end gap-2">
{tier.badge && !tier.popular && (
<Badge
variant="secondary"
className="bg-secondary/50 text-xs backdrop-blur-sm"
>
{tier.badge}
</Badge>
)}
{tier.discount && (
<div className="flex flex-col items-end gap-1">
<div className="rounded-md bg-green-500/10 px-2.5 py-1 ring-1 ring-green-500/20">
<p className="text-sm font-bold text-green-600 dark:text-green-400">
{tier.discount.percentage}% OFF
</p>
</div>
{tier.discount.label && (
<p className="text-xs font-medium text-green-600 dark:text-green-400">
{tier.discount.label}
</p>
)}
</div>
)}
</div>
</div>
<p className="min-h-[2.5rem] text-sm leading-relaxed text-muted-foreground">
{tier.description}
</p>
<div className="space-y-2">
<div className="flex items-baseline gap-2">
{tier.discount ? (
<>
<span className="bg-gradient-to-br from-foreground to-foreground/70 bg-clip-text text-6xl font-bold tracking-tight text-transparent">
$
{Math.round(
(period === "monthly"
? tier.price.monthly
: tier.price.yearly) *
(1 - tier.discount.percentage / 100),
)}
</span>
<div className="flex flex-col gap-0.5">
<span className="text-sm font-medium text-muted-foreground line-through">
$
{period === "monthly"
? tier.price.monthly
: tier.price.yearly}
</span>
<span className="text-xs text-muted-foreground">
/{period === "monthly" ? "mo" : "yr"}
</span>
</div>
</>
) : (
<>
<span className="bg-gradient-to-br from-foreground to-foreground/70 bg-clip-text text-6xl font-bold tracking-tight text-transparent">
$
{period === "monthly"
? tier.price.monthly
: tier.price.yearly}
</span>
<div className="flex flex-col">
<span className="text-sm font-medium text-muted-foreground">
/{period === "monthly" ? "mo" : "yr"}
</span>
</div>
</>
)}
</div>
{tier.discount && (
<div className="flex items-center gap-2">
<p className="text-sm font-semibold text-green-600 dark:text-green-400">
Save {tier.discount.percentage}%
</p>
<span className="text-xs text-muted-foreground">
• $
{Math.round(
(period === "monthly"
? tier.price.monthly
: tier.price.yearly) *
(tier.discount.percentage / 100),
)}{" "}
off
</span>
</div>
)}
{period === "yearly" &&
tier.price.monthly > 0 &&
!tier.discount && (
<p className="text-sm font-semibold text-green-600 dark:text-green-400">
Save $
{(
tier.price.monthly * 12 -
tier.price.yearly
).toFixed(0)}{" "}
per year
</p>
)}
</div>
<Button
onClick={tier.cta.onClick}
size="lg"
className={`group/btn relative w-full overflow-hidden font-medium transition-all duration-300 ${
tier.popular
? "bg-primary text-primary-foreground shadow-lg shadow-primary/25 hover:shadow-xl hover:shadow-primary/30"
: "border border-border/50 bg-secondary/50 text-secondary-foreground backdrop-blur-sm hover:bg-secondary/80 hover:shadow-lg"
}`}
>
<span className="relative z-10 flex items-center">
{tier.cta.text}
<ArrowRight className="ml-2 h-4 w-4 transition-transform group-hover/btn:translate-x-1" />
</span>
{tier.popular && (
<div className="absolute inset-0 -z-0 bg-gradient-to-r from-transparent via-white/10 to-transparent opacity-0 transition-opacity group-hover/btn:opacity-100" />
)}
</Button>
</CardHeader>
<CardContent className="space-y-4 pb-10">
<div className="relative">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-border/50" />
</div>
<div className="relative flex justify-center text-xs uppercase">
<span className="bg-card px-2 text-muted-foreground">
Features
</span>
</div>
</div>
<div className="space-y-4 pt-2">
{tier.features.map((feature, featureIndex) => (
<div
key={featureIndex}
className="flex items-start gap-3 transition-all hover:translate-x-1"
>
<div
className={`mt-0.5 flex h-5 w-5 flex-shrink-0 items-center justify-center rounded-full transition-all ${
feature.comingSoon
? "bg-amber-500/10 text-amber-600 ring-1 ring-amber-500/20 dark:text-amber-400"
: feature.included
? "bg-primary/15 text-primary ring-1 ring-primary/20"
: "bg-muted/50 text-muted-foreground/50"
}`}
>
{feature.comingSoon ? (
<Clock className="h-3.5 w-3.5" />
) : feature.included ? (
<Check className="h-3.5 w-3.5 stroke-[3]" />
) : (
<X className="h-3.5 w-3.5" />
)}
</div>
<span
className={`flex items-center gap-2 text-sm leading-relaxed ${
feature.comingSoon
? "font-medium text-foreground"
: feature.included
? "font-medium text-foreground"
: "text-muted-foreground/60 line-through"
}`}
>
{feature.text}
{feature.comingSoon && (
<Badge
variant="outline"
className="border-amber-500/30 bg-amber-500/10 px-1.5 py-0 text-[9px] font-medium text-amber-600 dark:text-amber-400"
>
Soon
</Badge>
)}
</span>
</div>
))}
</div>
</CardContent>
</Card>
</div>
))}
</div>
<div className="mt-20 animate-in fade-in slide-in-from-bottom-8 text-center duration-1000">
<p className="text-sm text-muted-foreground">
All plans include a 14-day free trial • No credit card required
</p>
</div>
</div>
</section>
);
}Update the import paths to match your project setup.
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader } from "@/components/ui/card";Usage
Basic Usage
import PricingToggle from "@/components/ui/pricing-toggle";
export default function PricingPage() {
return <PricingToggle />;
}With State Management
"use client";
import { useState } from "react";
import PricingToggle from "@/components/ui/pricing-toggle";
export default function PricingPage() {
const [period, setPeriod] = useState<"monthly" | "yearly">("monthly");
return (
<div>
<PricingToggle
defaultPeriod={period}
onPeriodChange={setPeriod}
/>
<p>Selected: {period}</p>
</div>
);
}Custom Labels and Savings
<PricingToggle
monthlyLabel="Pay Monthly"
yearlyLabel="Pay Annually"
savingsPercentage={25}
savingsLabel="25% Off"
/>With Pricing Table
"use client";
import { useState } from "react";
import PricingToggle from "@/components/ui/pricing-toggle";
import PricingTable3Tier from "@/components/ui/pricing-table-3tier";
export default function PricingPage() {
const [period, setPeriod] = useState<"monthly" | "yearly">("monthly");
return (
<div className="space-y-12">
<div className="text-center">
<h1 className="mb-4 text-4xl font-bold">Choose Your Plan</h1>
<p className="mb-8 text-muted-foreground">
Save 20% with annual billing
</p>
<PricingToggle
defaultPeriod={period}
onPeriodChange={setPeriod}
savingsPercentage={20}
/>
</div>
<PricingTable3Tier
billingPeriod={period}
tiers={[/* your tiers */]}
/>
</div>
);
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
defaultPeriod | "monthly" | "yearly" | "monthly" | Initial billing period |
onPeriodChange | (period) => void | undefined | Callback when period changes |
savingsPercentage | number | 20 | Percentage shown in savings badge |
monthlyLabel | string | "Monthly" | Label for monthly option |
yearlyLabel | string | "Yearly" | Label for yearly option |
savingsLabel | string | undefined | Custom savings badge text |
TypeScript Interface
interface PricingToggleProps {
defaultPeriod?: "monthly" | "yearly";
onPeriodChange?: (period: "monthly" | "yearly") => void;
savingsPercentage?: number;
monthlyLabel?: string;
yearlyLabel?: string;
savingsLabel?: string;
}Customization
Change Colors
// Modify the selected state background
<div className="absolute inset-0 rounded-lg bg-gradient-to-br from-blue-500/10 to-blue-500/5 ring-1 ring-blue-500/20" />
// Modify the toggle switch
<div className="bg-gradient-to-br from-blue-600 to-blue-500" />Change Savings Badge Style
<Badge className="border-emerald-500/30 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400">
Save {savingsPercentage}%
</Badge>Add Icons
import { Calendar, CalendarCheck } from "lucide-react";
<button>
<Calendar className="mr-2 h-4 w-4" />
{monthlyLabel}
</button>Use Cases
Perfect for:
- SaaS pricing pages
- Subscription services
- Membership plans
- Software products
- API pricing
- Service tiers
- Tool subscriptions
- Platform pricing