Docs
Pricing Toggle

Pricing Toggle

A clean, professional monthly/yearly billing toggle with smooth animations and savings badge. Perfect for pricing pages.

Pricing

Simple, transparent pricing

Choose the perfect plan for your needs. Switch between monthly and yearly billing.

Starter

Free

Perfect for individuals and small projects

$0
/mo
Features
Up to 3 projects
Basic analytics
Community support
1 GB storage
API access
Advanced features
Priority support
Custom integrations
Most Popular

Pro

Best for growing teams and businesses

$29
/mo
Features
Unlimited projects
Advanced analytics
Priority support
50 GB storage
API access
Advanced features
Team collaboration
Soon
Custom integrations

Enterprise

Custom

For large organizations with custom needs

$99
/mo
Features
Unlimited everything
Advanced analytics
24/7 dedicated support
Unlimited storage
API access
Advanced features
Team collaboration
Custom integrations
Soon

All plans include a 14-day free trial • No credit card required

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

PropTypeDefaultDescription
defaultPeriod"monthly" | "yearly""monthly"Initial billing period
onPeriodChange(period) => voidundefinedCallback when period changes
savingsPercentagenumber20Percentage shown in savings badge
monthlyLabelstring"Monthly"Label for monthly option
yearlyLabelstring"Yearly"Label for yearly option
savingsLabelstringundefinedCustom 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