Pricing Table Six

PreviousNext

A modern pricing table component with contact us plan

Docs
billingsdkblock

Preview

Loading preview…
registry/billingsdk/pricing-table-six.tsx
"use client";

import { useState } from "react";
import { Check } from "lucide-react";
import { Button } from "@/components/ui/button";
import { AnimatePresence, motion } from "framer-motion";

export interface PlanProps {
  id: string;
  title: string;
  description: string;
  monthlyPrice: number;
  yearlyPrice: number;
  features: string[];
  isFeatured?: boolean;
  isCustom?: boolean;
}

export interface PricingTableSixProps {
  plans: PlanProps[];
  onPlanSelect: (planId: string) => void;
}

const gradientFrom = ["from-chart-1/70", "from-chart-2/70", "from-chart-3/70"];

const getDiscountPercent = (plan: PlanProps) => {
  return Math.min(
    100,
    Math.max(
      0,
      Math.round((1 - plan.yearlyPrice / (plan.monthlyPrice * 12)) * 100),
    ),
  );
};

export function PricingTableSix({ plans, onPlanSelect }: PricingTableSixProps) {
  const [isYearly, setIsYearly] = useState(false);

  return (
    <section className="px-4 py-16 sm:px-6 lg:px-8">
      <div className="mx-auto max-w-7xl">
        {/* Header */}
        <div className="mb-16 grid items-start gap-8 lg:grid-cols-2 lg:gap-16">
          <div>
            <h2 className="text-foreground text-4xl leading-tight font-bold tracking-tight sm:text-5xl lg:text-[3.5rem]">
              Tailored plans for
              <span className="font-light italic">every stage</span>
            </h2>
          </div>

          <div className="lg:pt-4">
            <p className="text-muted-foreground mb-6 text-base leading-relaxed">
              No matter where you are in your journey, find a plan that fits
              your goals and budget.
            </p>

            {/* Toggle */}

            {/* Toggle with layoutId */}
            <div className="bg-primary-foreground/70 relative inline-flex items-center rounded-full p-1.5 dark:!shadow-[inset_0_1.5px_0_color-mix(in_oklch,_var(--primary)_15%,_transparent)]">
              <button
                onClick={() => setIsYearly(false)}
                className={`text-foreground relative z-10 cursor-pointer rounded-full px-4 py-2 text-sm font-medium ${
                  !isYearly
                    ? "text-foreground border-muted-foreground/10 border"
                    : "text-muted-foreground"
                }`}
                aria-pressed={!isYearly}
              >
                {!isYearly && (
                  <motion.div
                    layoutId="toggle-indicator"
                    className="bg-primary-foreground absolute inset-0 rounded-full shadow-sm"
                    transition={{ type: "spring", stiffness: 300, damping: 30 }}
                  />
                )}
                <span className="relative z-10">Monthly</span>
              </button>

              <button
                onClick={() => setIsYearly(true)}
                className={`text-foreground relative z-10 cursor-pointer rounded-full px-4 py-2 text-sm font-medium ${
                  isYearly
                    ? "text-foreground border-muted-foreground/10 border"
                    : "text-muted-foreground"
                }`}
                aria-pressed={isYearly}
              >
                {isYearly && (
                  <motion.div
                    layoutId="toggle-indicator"
                    className="bg-primary-foreground absolute inset-0 rounded-full shadow-sm"
                    transition={{ type: "spring", stiffness: 300, damping: 30 }}
                  />
                )}
                <span className="relative z-10">Yearly</span>
              </button>
            </div>
          </div>
        </div>

        {/* Pricing Cards */}
        <div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3 lg:gap-7">
          {plans.map((plan, index) => (
            <div
              key={plan.id}
              className={`border-primary-foreground relative rounded-3xl border bg-gradient-to-b p-6 shadow-xl transition-all duration-300 hover:-translate-y-1 hover:scale-[1.01] ${gradientFrom[index]} via-primary-foreground/10 to-primary-foreground from-[0%] via-[40%] to-[100%]`}
            >
              {/* Most Popular Badge */}
              {plan.isFeatured && (
                <div className="absolute -top-3 left-1/2 -translate-x-1/2 transform">
                  <div className="bg-primary-foreground text-foreground ring-muted-foreground/50 rounded-full px-3 py-1 text-xs font-medium whitespace-nowrap ring-1">
                    Most popular
                  </div>
                </div>
              )}

              {/* Title and Description */}
              <div className="mb-6">
                <div className="mb-2 flex items-center justify-between">
                  <h3 className="text-foreground text-xl font-bold">
                    {plan.title}
                  </h3>
                  {isYearly &&
                    plan.monthlyPrice > 0 &&
                    plan.yearlyPrice < plan.monthlyPrice * 12 && (
                      <motion.div
                        className=""
                        initial={{ opacity: 0, y: 10 }}
                        animate={{ opacity: 1, y: 0 }}
                        exit={{ opacity: 0, y: -10 }}
                        transition={{ duration: 0.3 }}
                      >
                        <span className="bg-primary-foreground text-foreground shadow-muted-foreground/40 inline-block rounded-full px-2 py-1 text-xs whitespace-nowrap shadow-sm">
                          Save {getDiscountPercent(plan)}%
                        </span>
                      </motion.div>
                    )}
                </div>
                <p className="text-muted-foreground text-sm">
                  {plan.description}
                </p>
              </div>

              {/* Price */}
              <div className="mb-8">
                <div className="flex items-baseline">
                  <span className="text-muted-foreground text-base">$</span>
                  <AnimatePresence mode="wait" initial={false}>
                    <motion.span
                      key={isYearly ? "yearly-price" : "monthly-price"}
                      className="text-foreground ml-1 text-5xl font-bold"
                      initial={{ opacity: 0, y: 8 }}
                      animate={{ opacity: 1, y: 0 }}
                      exit={{ opacity: 0, y: -8 }}
                      transition={{ duration: 0.2 }}
                    >
                      {isYearly ? plan.yearlyPrice : plan.monthlyPrice}
                    </motion.span>
                  </AnimatePresence>
                  <AnimatePresence mode="wait" initial={false}>
                    <motion.span
                      key={isYearly ? "per-year" : "per-month"}
                      className="text-muted-foreground ml-2 text-sm"
                      initial={{ opacity: 0, y: 8 }}
                      animate={{ opacity: 1, y: 0 }}
                      exit={{ opacity: 0, y: -8 }}
                      transition={{ duration: 0.2 }}
                    >
                      / {isYearly ? "year" : "month"}
                    </motion.span>
                  </AnimatePresence>
                </div>
              </div>

              {/* CTA Button */}
              <div className="mb-8">
                <Button
                  className={`bg-foreground text-primary-foreground hover:bg-foreground/90 border-primary-foreground h-12 w-full rounded-xl border font-medium transition-all duration-200 hover:cursor-pointer ${!plan.isFeatured ? "bg-foreground/80 text-primary-foreground hover:bg-foreground/70" : ""} `}
                  aria-label={`Start ${plan.title} plan${(isYearly ? plan.yearlyPrice : plan.monthlyPrice) === 0 ? " — free" : ""}`}
                  onClick={() => onPlanSelect(plan.id)}
                >
                  {plan.isCustom ? "Contact team" : "Get started"}
                </Button>
              </div>

              {/* Features */}
              <div>
                <h4 className="text-muted-foreground mb-4 text-sm font-medium">
                  What's included:
                </h4>
                <ul className="space-y-3">
                  {plan.features.map((feature, index) => (
                    <li key={index} className="flex items-start">
                      <div className="bg-primary-foreground mt-0.5 mr-3 flex h-4 w-4 flex-shrink-0 items-center justify-center rounded-full">
                        <Check className="text-foreground h-2.5 w-2.5" />
                      </div>
                      <span className="text-foreground text-sm leading-relaxed">
                        {feature}
                      </span>
                    </li>
                  ))}
                </ul>
              </div>
            </div>
          ))}
        </div>
      </div>
    </section>
  );
}

Installation

npx shadcn@latest add @billingsdk/pricing-table-six

Usage

import { PricingTableSix } from "@/components/pricing-table-six"
<PricingTableSix />