Simple, transparent pricing
Features
- ✅ 3-tier layout - Starter, Pro, Enterprise structure
- ✅ Modern design - Sophisticated gradients and refined styling
- ✅ Discount support - Show promotional discounts with custom labels
- ✅ Coming soon badges - Mark features as "Coming Soon" with amber styling
- ✅ Popular badge - Highlight recommended plan with glow effect
- ✅ Billing toggle - Switch between monthly/yearly pricing
- ✅ Savings calculator - Shows yearly savings automatically
- ✅ Feature comparison - Check/X/Clock icons for feature states
- ✅ Responsive design - 3 columns → 1 column on mobile
- ✅ Shadcn components - Built with Card, Badge, Button
- ✅ TypeScript support - Full type safety
- ✅ Production-ready - Copy and paste to use
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, Sparkles, X } from "lucide-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 PricingTable3TierProps {
badge?: string;
headline: string;
description?: string;
billingPeriod?: "monthly" | "yearly";
tiers: PricingTier[];
}
export function PricingTable3Tier({
badge,
headline,
description,
billingPeriod = "monthly",
tiers,
}: PricingTable3TierProps) {
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-20 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 text-lg leading-relaxed text-muted-foreground duration-1000 sm:text-xl">
{description}
</p>
)}
</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}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 overflow-visible 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(
(billingPeriod === "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">
$
{billingPeriod === "monthly"
? tier.price.monthly
: tier.price.yearly}
</span>
<span className="text-xs text-muted-foreground">
/{billingPeriod === "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">
$
{billingPeriod === "monthly"
? tier.price.monthly
: tier.price.yearly}
</span>
<div className="flex flex-col">
<span className="text-sm font-medium text-muted-foreground">
/{billingPeriod === "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(
(billingPeriod === "monthly"
? tier.price.monthly
: tier.price.yearly) *
(tier.discount.percentage / 100),
)}{" "}
off
</span>
</div>
)}
{billingPeriod === "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.
Usage
Basic Usage
import PricingTable3Tier from "@/components/ui/pricing-table-3tier";
export default function PricingPage() {
return (
<PricingTable3Tier
headline="Simple pricing"
tiers={[
{
name: "Starter",
description: "For individuals",
price: { monthly: 0, yearly: 0 },
features: [
{ text: "Up to 3 projects", included: true },
{ text: "Basic support", included: true },
{ text: "API access", included: false },
{ text: "Advanced features", included: true, comingSoon: true },
],
cta: { text: "Get Started", onClick: () => {} },
},
{
name: "Pro",
description: "For teams",
price: { monthly: 29, yearly: 290 },
discount: {
percentage: 20,
label: "Limited Offer",
},
popular: true,
features: [
{ text: "Unlimited projects", included: true },
{ text: "Priority support", included: true },
{ text: "API access", included: true },
],
cta: { text: "Start Trial", onClick: () => {} },
},
{
name: "Enterprise",
description: "For organizations",
price: { monthly: 99, yearly: 990 },
features: [
{ text: "Unlimited everything", included: true },
{ text: "24/7 support", included: true },
{ text: "API access", included: true },
],
cta: { text: "Contact Sales", onClick: () => {} },
},
]}
/>
);
}With All Features
<PricingTable3Tier
badge="Pricing"
headline="Simple, transparent pricing"
description="Choose the perfect plan for your needs. All plans include a 14-day free trial."
billingPeriod="monthly"
tiers={[
{
name: "Starter",
description: "Perfect for individuals and small projects",
price: { monthly: 0, yearly: 0 },
badge: "Free",
features: [
{ text: "Up to 3 projects", included: true },
{ text: "Basic analytics", included: true },
{ text: "Community support", included: true },
{ text: "API access", included: false },
],
cta: {
text: "Get Started",
onClick: () => console.log("Starter"),
},
},
// ... more tiers
]}
/>With Router Navigation
"use client";
import { useRouter } from "next/navigation";
import PricingTable3Tier from "@/components/ui/pricing-table-3tier";
export default function PricingPage() {
const router = useRouter();
return (
<PricingTable3Tier
headline="Choose your plan"
tiers={[
{
name: "Pro",
description: "For growing teams",
price: { monthly: 29, yearly: 290 },
popular: true,
features: [/* ... */],
cta: {
text: "Start Free Trial",
onClick: () => router.push("/signup?plan=pro"),
},
},
]}
/>
);
}Props
PricingTable3TierProps
| Prop | Type | Default | Description |
|---|---|---|---|
badge | string | undefined | Optional badge text above headline |
headline | string | Required | Main section headline |
description | string | undefined | Optional description below headline |
billingPeriod | "monthly" | "yearly" | "monthly" | Current billing period |
tiers | PricingTier[] | Required | Array of pricing tier objects |
PricingTier Object
| Prop | Type | Description |
|---|---|---|
name | string | Tier name (e.g., "Starter", "Pro") |
description | string | Short description of the tier |
price | object | Price object with monthly and yearly |
price.monthly | number | Monthly price |
price.yearly | number | Yearly price |
discount | object | Optional discount configuration |
discount.percentage | number | Discount percentage (e.g., 20 for 20%) |
discount.label | string | Optional custom label (e.g., "Limited Offer") |
badge | string | Optional badge (e.g., "Free", "Custom") |
popular | boolean | Mark as popular/recommended |
features | PricingFeature[] | Array of feature objects |
cta | object | Call-to-action button |
cta.text | string | Button text |
cta.href | string | Optional link URL |
cta.onClick | () => void | Optional click handler |
PricingFeature Object
| Prop | Type | Description |
|---|---|---|
text | string | Feature description |
included | boolean | Whether feature is included |
comingSoon | boolean | Optional - Mark feature as coming soon |
TypeScript Interface
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 PricingTable3TierProps {
badge?: string;
headline: string;
description?: string;
billingPeriod?: "monthly" | "yearly";
tiers: PricingTier[];
}Advanced Features
Discount Badges
Add promotional discounts to any tier:
{
name: "Pro",
price: { monthly: 29, yearly: 290 },
discount: {
percentage: 20,
label: "Limited Offer", // Optional custom label
},
// ... other props
}Without a custom label, it shows "Save 20%":
discount: {
percentage: 15, // Shows "Save 15%"
}Coming Soon Features
Mark features that are in development:
features: [
{ text: "Team collaboration", included: true, comingSoon: true },
{ text: "API access", included: true },
{ text: "Custom integrations", included: false },
]Features with comingSoon: true display:
- Clock icon (amber color)
- "Soon" badge next to the feature text
- No strikethrough styling
Customization
Add Billing Toggle
Create a toggle to switch between monthly and yearly:
"use client";
import { useState } from "react";
import { Switch } from "@/components/ui/switch";
export default function PricingPage() {
const [billingPeriod, setBillingPeriod] = useState<"monthly" | "yearly">("monthly");
return (
<div>
<div className="mb-8 flex items-center justify-center gap-4">
<span className={billingPeriod === "monthly" ? "font-semibold" : ""}>
Monthly
</span>
<Switch
checked={billingPeriod === "yearly"}
onCheckedChange={(checked) =>
setBillingPeriod(checked ? "yearly" : "monthly")
}
/>
<span className={billingPeriod === "yearly" ? "font-semibold" : ""}>
Yearly
</span>
<Badge>Save 20%</Badge>
</div>
<PricingTable3Tier
billingPeriod={billingPeriod}
tiers={[/* ... */]}
/>
</div>
);
}Change Popular Badge Style
{tier.popular && (
<div className="absolute -top-4 left-1/2 -translate-x-1/2">
<Badge className="bg-gradient-to-r from-primary to-purple-600 px-4 py-1">
Most Popular
</Badge>
</div>
)}Customize Feature Icons
The component uses three icon states:
// Included feature (green check)
<Check className="h-3.5 w-3.5 stroke-[3]" />
// Coming soon feature (amber clock)
<Clock className="h-3.5 w-3.5" />
// Not included feature (muted X)
<X className="h-3.5 w-3.5" />Change Discount Badge Style
{tier.discount && (
<div className="inline-flex items-center gap-1.5 rounded-full bg-gradient-to-r from-green-600 to-green-500 px-3 py-1">
<Sparkles className="h-3 w-3" />
{tier.discount.label || `Save ${tier.discount.percentage}%`}
</div>
)}Use Cases
Perfect for:
- SaaS pricing pages
- Subscription services
- Software products
- API pricing
- Service tiers
- Membership plans
- Tool subscriptions
- Platform pricing
Best Practices
- Keep it simple - 3 tiers is optimal for decision-making
- Highlight one plan - Mark your recommended tier as popular
- Clear features - Use simple, benefit-focused language
- Consistent features - Show same features across all tiers
- Yearly discount - Offer 15-20% discount for annual billing
- Free tier - Consider offering a free starter plan
- Clear CTAs - Use action-oriented button text