Pricing

Pricing section with annual/monthly toggle.

Default Usage

import { Pricing } from '@/components/landing';

<Pricing />

Location: src/components/landing/Pricing.tsx

Features

What's Included

Starterbase comes with:

Customization

Edit the plans array in Pricing.tsx:

const plans: PricingPlan[] = [
  {
    nameKey: 'free.name',
    descriptionKey: 'free.description',
    price: { monthly: 0, annual: 0 },
    features: ['free.feature1', 'free.feature2', 'free.feature3'],
    ctaKey: 'free.cta',
    href: '/register',
  },
  {
    nameKey: 'pro.name',
    price: { monthly: 29, annual: 24 },
    highlighted: true,  // Adds "Most Popular" styling
    badge: 'pro.badge',
    features: ['pro.feature1', 'pro.feature2', 'pro.feature3', 'pro.feature4'],
    // feature4 = "Priority email support"
    // ...
  },
];

Variants

Variant 1: Three Plans (Default)

Standard 3-column pricing with toggle.

// src/components/landing/Pricing.tsx - Default implementation

Best for: Most products, clear tier differentiation


Variant 2: Two Plans (Simple)

Simplified 2-plan pricing for smaller products.

// src/components/landing/PricingSimple.tsx
'use client';

import { useState } from 'react';
import { useTranslations } from 'next-intl';
import Link from 'next/link';
import { useScrollAnimation } from '@/hooks';
import { Button, Card, CardContent } from '@/components/ui';
import { CheckIcon } from '@/components/icons';

export default function PricingSimple() {
  const t = useTranslations('landing.pricing');
  const [isAnnual, setIsAnnual] = useState(true);
  const { elementRef, isVisible } = useScrollAnimation();

  const plans = [
    {
      name: 'Free',
      price: { monthly: 0, annual: 0 },
      description: 'Perfect for trying out',
      features: [
        'Up to 3 projects',
        'Basic features',
        'Community support',
        '5GB storage',
      ],
      cta: 'Get Started',
      href: '/register',
      popular: false,
    },
    {
      name: 'Pro',
      price: { monthly: 29, annual: 24 },
      description: 'For professionals',
      features: [
        'Unlimited projects',
        'All features',
        'Priority email support',
        '100GB storage',
        'Advanced analytics',
        'Custom domain',
      ],
      cta: 'Start Free Trial',
      href: '/register?plan=pro',
      popular: true,
    },
  ];

  return (
    <section id="pricing" className="py-20 sm:py-32 bg-white dark:bg-zinc-950">
      <div className="container mx-auto px-4">
        <div
          ref={elementRef as React.RefObject<HTMLDivElement>}
          className={`scroll-fade-in ${isVisible ? 'visible' : ''}`}
        >
          <div className="text-center mb-12">
            <h2 className="text-3xl sm:text-4xl font-bold mb-4">
              {t('title')}
            </h2>
            <p className="text-zinc-600 dark:text-zinc-400 text-lg">
              {t('subtitle')}
            </p>
          </div>

          {/* Billing toggle */}
          <div className="flex items-center justify-center gap-4 mb-12">
            <span className={!isAnnual ? 'font-semibold' : 'text-zinc-500'}>
              Monthly
            </span>
            <button
              onClick={() => setIsAnnual(!isAnnual)}
              className={`relative w-14 h-7 rounded-full transition-colors ${
                isAnnual
                  ? 'bg-zinc-900 dark:bg-zinc-100'
                  : 'bg-zinc-300 dark:bg-zinc-700'
              }`}
            >
              <span
                className={`absolute top-1 w-5 h-5 rounded-full bg-white dark:bg-zinc-900 transition-transform ${
                  isAnnual ? 'translate-x-8' : 'translate-x-1'
                }`}
              />
            </button>
            <span className={isAnnual ? 'font-semibold' : 'text-zinc-500'}>
              Annual
              <span className="ml-2 text-sm text-green-600">Save 20%</span>
            </span>
          </div>

          {/* Plans */}
          <div className="grid md:grid-cols-2 gap-8 max-w-4xl mx-auto">
            {plans.map((plan, index) => (
              <Card
                key={index}
                className={`relative ${
                  plan.popular
                    ? 'ring-2 ring-zinc-900 dark:ring-zinc-100 md:scale-105'
                    : ''
                }`}
              >
                {plan.popular && (
                  <div className="absolute -top-4 left-1/2 -translate-x-1/2 px-4 py-1 bg-zinc-900 dark:bg-zinc-100 text-white dark:text-zinc-900 text-sm font-semibold rounded-full">
                    Most Popular
                  </div>
                )}
                <CardContent className="p-8">
                  <h3 className="text-2xl font-bold mb-2">{plan.name}</h3>
                  <p className="text-zinc-600 dark:text-zinc-400 mb-6">
                    {plan.description}
                  </p>
                  <div className="mb-8">
                    <span className="text-5xl font-bold">
                      ${isAnnual ? plan.price.annual : plan.price.monthly}
                    </span>
                    <span className="text-zinc-600 dark:text-zinc-400 ml-2">
                      /month
                    </span>
                    {isAnnual && plan.price.annual < plan.price.monthly && (
                      <div className="text-sm text-green-600 mt-1">
                        ${plan.price.annual * 12}/year
                      </div>
                    )}
                  </div>
                  <ul className="space-y-4 mb-8">
                    {plan.features.map((feature, i) => (
                      <li key={i} className="flex items-start gap-3">
                        <CheckIcon className="w-5 h-5 text-green-600 flex-shrink-0 mt-0.5" />
                        <span>{feature}</span>
                      </li>
                    ))}
                  </ul>
                  <Button
                    as={Link}
                    href={plan.href}
                    color={plan.popular ? 'dark' : undefined}
                    outline={!plan.popular}
                    className="w-full"
                  >
                    {plan.cta}
                  </Button>
                </CardContent>
              </Card>
            ))}
          </div>

          {/* FAQ link */}
          <p className="text-center mt-12 text-zinc-600 dark:text-zinc-400">
            Have questions?{' '}
            <Link href="#faq" className="text-zinc-900 dark:text-white underline">
              Check our FAQ
            </Link>
          </p>
        </div>
      </div>
    </section>
  );
}

Best for: Simple products, clear differentiation, solopreneurs


Variant 3: Comparison Table

Detailed feature comparison across plans.

// src/components/landing/PricingComparison.tsx
'use client';

import React, { useState } from 'react';
import { useTranslations } from 'next-intl';
import Link from 'next/link';
import { Button } from '@/components/ui';
import { CheckIcon, XMarkIcon } from '@/components/icons';

export default function PricingComparison() {
  const [isAnnual, setIsAnnual] = useState(true);

  const plans = [
    { name: 'Free', price: { monthly: 0, annual: 0 } },
    { name: 'Pro', price: { monthly: 29, annual: 24 } },
    { name: 'Enterprise', price: { monthly: 99, annual: 79 } },
  ];

  const features = [
    {
      category: 'Core Features',
      items: [
        { name: 'Projects', values: ['3', 'Unlimited', 'Unlimited'] },
        { name: 'Storage', values: ['5GB', '100GB', '1TB'] },
        { name: 'Team members', values: ['1', '5', 'Unlimited'] },
      ],
    },
    {
      category: 'Advanced Features',
      items: [
        { name: 'Priority email support', values: [false, true, true] },
        { name: 'Custom domain', values: [false, true, true] },
        { name: 'SSO', values: [false, false, true] },
        { name: 'API access', values: [false, true, true] },
      ],
    },
  ];

  return (
    <section id="pricing" className="py-20 sm:py-32">
      <div className="container mx-auto px-4">
        <div className="text-center mb-12">
          <h2 className="text-3xl sm:text-4xl font-bold mb-4">
            Compare Plans
          </h2>
          <p className="text-zinc-600 dark:text-zinc-400 text-lg">
            Choose the perfect plan for your needs
          </p>
        </div>

        {/* Billing toggle */}
        <div className="flex items-center justify-center gap-4 mb-12">
          <span className={!isAnnual ? 'font-semibold' : 'text-zinc-500'}>
            Monthly
          </span>
          <button
            onClick={() => setIsAnnual(!isAnnual)}
            className={`relative w-14 h-7 rounded-full transition-colors ${
              isAnnual
                ? 'bg-zinc-900 dark:bg-zinc-100'
                : 'bg-zinc-300 dark:bg-zinc-700'
            }`}
          >
            <span
              className={`absolute top-1 w-5 h-5 rounded-full bg-white dark:bg-zinc-900 transition-transform ${
                isAnnual ? 'translate-x-8' : 'translate-x-1'
              }`}
            />
          </button>
          <span className={isAnnual ? 'font-semibold' : 'text-zinc-500'}>
            Annual <span className="text-green-600">Save 20%</span>
          </span>
        </div>

        {/* Comparison table */}
        <div className="max-w-6xl mx-auto overflow-x-auto">
          <table className="w-full border-collapse">
            {/* Header with pricing */}
            <thead>
              <tr>
                <th className="text-left p-4 border-b border-zinc-200 dark:border-zinc-800">
                  Features
                </th>
                {plans.map((plan, index) => (
                  <th
                    key={index}
                    className="p-4 border-b border-zinc-200 dark:border-zinc-800"
                  >
                    <div className="text-center">
                      <div className="font-bold text-lg mb-2">{plan.name}</div>
                      <div className="text-3xl font-bold mb-4">
                        $
                        {isAnnual ? plan.price.annual : plan.price.monthly}
                        <span className="text-sm font-normal text-zinc-600 dark:text-zinc-400">
                          /mo
                        </span>
                      </div>
                      <Button
                        as={Link}
                        href={`/register?plan=${plan.name.toLowerCase()}`}
                        color={index === 1 ? 'dark' : undefined}
                        outline={index !== 1}
                        className="w-full"
                      >
                        Get Started
                      </Button>
                    </div>
                  </th>
                ))}
              </tr>
            </thead>

            {/* Feature rows */}
            <tbody>
              {features.map((category, catIndex) => (
                <React.Fragment key={catIndex}>
                  <tr>
                    <td
                      colSpan={plans.length + 1}
                      className="p-4 bg-zinc-50 dark:bg-zinc-900/50 font-semibold border-y border-zinc-200 dark:border-zinc-800"
                    >
                      {category.category}
                    </td>
                  </tr>
                  {category.items.map((item, itemIndex) => (
                    <tr
                      key={itemIndex}
                      className="border-b border-zinc-200 dark:border-zinc-800"
                    >
                      <td className="p-4 text-zinc-700 dark:text-zinc-300">
                        {item.name}
                      </td>
                      {item.values.map((value, valueIndex) => (
                        <td key={valueIndex} className="p-4 text-center">
                          {typeof value === 'boolean' ? (
                            value ? (
                              <CheckIcon className="w-5 h-5 text-green-600 mx-auto" />
                            ) : (
                              <XMarkIcon className="w-5 h-5 text-zinc-300 dark:text-zinc-700 mx-auto" />
                            )
                          ) : (
                            <span className="text-zinc-900 dark:text-white font-medium">
                              {value}
                            </span>
                          )}
                        </td>
                      ))}
                    </tr>
                  ))}
                </React.Fragment>
              ))}
            </tbody>
          </table>
        </div>
      </div>
    </section>
  );
}

Best for: Complex products, B2B applications, when features need detailed comparison


Choosing a Variant

Variant Use When
Three Plans Standard apps with free, pro, enterprise tiers
Two Plans (Simple) Simple product, clear free vs paid distinction
Comparison Table Many features to compare, enterprise sales focus