Components

Pricing Card

A responsive card component for displaying a single pricing tier. It adapts its display based on the selected billing interval.

Starter
For individuals and small teams just getting started.

$0/month

Completely free. No credit card required.


  • 5 Projects
  • Basic Analytics
  • 2GB Storage
  • Email Support
  • Custom Domain
Most Popular
Pro
For growing businesses that need more power and support.

$20/month

per user / month


  • Unlimited Projects
  • Advanced Analytics
  • 50GB Storage
  • Priority Email Support
  • Custom Domain
Enterprise
For large organizations with custom requirements.

Custom

Volume discounts available


  • Everything in Pro
  • SAML/SSO Login
  • Dedicated Account Manager
  • On-premise Deployment
  • 24/7 Phone Support
Developer
Features with tooltips for additional context.

$49/month


  • Core Functionality
  • API Access
  • Rate Limiting
  • Service Level Agreement (SLA)
Team
Highlight newly launched features to drive adoption.

$99/month


  • Standard Reporting
  • User Roles
  • AI Assistant
  • Real-time Collaboration
API Access
Usage-based pricing for our powerful API.

$15/month

per 10,000 requests


  • All API Endpoints
  • Webhooks
  • Standard Support
Lifetime Deal
A one-time purchase for lifetime access.

$499/month

One-time payment


  • All Current Features
  • All Future Updates
  • Community Support
Basic
A simple, no-frills plan for essential needs.

$5/month


  • One Feature
  • Another Feature
Dashboard Upgrade
Upgrade from within your app dashboard.

$10/month


  • Unlock Feature X
  • Unlock Feature Y

Installation

CLI

Installation via the CLI is coming soon. For now, please follow the manual installation instructions below.

Manual

No additional dependencies are required. Copy and paste the following code into your project.

'use client';

import * as React from 'react';
import Link from 'next/link';
import { CheckCircle2, Info, XCircle } from 'lucide-react';
import type { VariantProps } from 'class-variance-authority';

import { cn } from '@/lib/utils';
import { Badge } from '@/components/ui/badge';
import { Button, buttonVariants } from '@/components/ui/button';
import {
  Card,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from '@/components/ui/card';
import {
  Tooltip,
  TooltipContent,
  TooltipProvider,
  TooltipTrigger,
} from '@/components/ui/tooltip';

type PricingInterval = 'month' | 'year';

interface PricingFeature {
  text: string;
  included: boolean;
  tooltip?: string;
  isNew?: boolean;
}

interface Price {
  monthly: number;
  yearly: number;
}

/**
 * Props for the PricingCard component.
 */
interface PricingCardProps extends React.HTMLAttributes<HTMLDivElement> {
  /** The currently selected billing interval. */
  interval: PricingInterval;
  /** The name of the pricing plan. */
  name: string;
  /** A short description of the plan. */
  description: string;
  /** The price, either a structured object for monthly/yearly costs or 'Custom' for enterprise plans. */
  price: Price | 'Custom';
  /** An optional subtitle for the price, perfect for per-seat costs or other contextual info. */
  priceSubtitle?: React.ReactNode;
  /** A list of features included in the plan. */
  features: PricingFeature[];
  /** Configuration for the call-to-action button. */
  cta: {
    text: string;
    href: string;
    variant?: VariantProps<typeof buttonVariants>['variant'];
  };
  /** Optional text for a badge, like 'Most Popular'. */
  badgeText?: string;
  /** If true, the card will be visually highlighted as the featured plan. */
  isFeatured?: boolean;
}

// NOTE: Memoizing the feature list item prevents re-rendering of the entire list
// when parent state changes, such as the billing interval.
const PricingFeature = React.memo(
  ({ feature }: { feature: PricingFeature }) => (
    <li
      className={cn(
        'flex items-center gap-3 text-sm text-muted-foreground',
        !feature.included && 'opacity-60'
      )}
    >
      {feature.included ? (
        <CheckCircle2 className='h-5 w-5 shrink-0 text-primary' />
      ) : (
        <XCircle className='h-5 w-5 shrink-0' />
      )}
      <span
        className={cn(
          'flex-1',
          feature.isNew && 'font-semibold text-foreground'
        )}
      >
        {feature.text}
      </span>
      {feature.tooltip && (
        <TooltipProvider>
          <Tooltip delayDuration={300}>
            <TooltipTrigger asChild>
              <button
                type='button'
                className='cursor-default rounded-full'
                aria-label='More info'
              >
                <Info className='h-4 w-4 text-muted-foreground' />
              </button>
            </TooltipTrigger>
            <TooltipContent>
              <p>{feature.tooltip}</p>
            </TooltipContent>
          </Tooltip>
        </TooltipProvider>
      )}
    </li>
  )
);
PricingFeature.displayName = 'PricingFeature';

/**
 * A responsive card component for displaying a single pricing tier.
 * It adapts its display based on the selected billing interval.
 */
const PricingCard = React.forwardRef<HTMLDivElement, PricingCardProps>(
  (
    {
      className,
      interval,
      name,
      description,
      price,
      priceSubtitle,
      features,
      cta,
      badgeText,
      isFeatured = false,
      ...props
    },
    ref
  ) => {
    const getPriceText = React.useCallback(() => {
      if (price === 'Custom') return 'Custom';
      const amount = interval === 'year' ? price.yearly / 12 : price.monthly;
      return amount.toLocaleString('en-US', {
        style: 'currency',
        currency: 'USD',
        minimumFractionDigits: 0,
        // WHY: Allow decimal points for prices calculated from an annual total (e.g., $99/year -> $8.25/month).
        maximumFractionDigits: 2,
      });
    }, [price, interval]);

    const priceText = getPriceText();

    return (
      <Card
        ref={ref}
        className={cn(
          'flex h-full flex-col transition-all duration-300 ease-in-out hover:scale-[1.02] hover:shadow-xl',
          isFeatured
            ? 'relative ring-2 ring-primary shadow-2xl shadow-primary/10'
            : 'border-border',
          className
        )}
        {...props}
      >
        {badgeText && (
          <Badge
            className='absolute right-4 top-4'
            variant={isFeatured ? 'default' : 'secondary'}
          >
            {badgeText}
          </Badge>
        )}

        <CardHeader className='flex-shrink-0'>
          <CardTitle>{name}</CardTitle>
          <CardDescription>{description}</CardDescription>
        </CardHeader>

        <CardContent className='flex flex-1 flex-col'>
          <div className='mb-6'>
            <p className='flex items-baseline' aria-live='polite'>
              <span
                // WHY: The key is crucial here. It forces React to re-mount this
                // element when the interval changes, which re-triggers the fade-in animation.
                key={`${name}-${interval}-price`}
                className='text-5xl font-bold tracking-tight text-foreground animate-in fade-in'
              >
                {priceText}
              </span>
              {price !== 'Custom' && (
                <span className='ml-1.5 text-muted-foreground'>/month</span>
              )}
            </p>
            {priceSubtitle && (
              <p className='mt-1 text-sm text-muted-foreground animate-in fade-in'>
                {priceSubtitle}
              </p>
            )}
            {price !== 'Custom' && interval === 'year' && (
              <p
                key={`${name}-annual-billing`}
                className='mt-1 text-sm text-muted-foreground animate-in fade-in'
              >
                Billed ${price.yearly.toLocaleString()} annually
              </p>
            )}
          </div>

          <hr className='mb-6 border-border/60' />

          <ul className='space-y-4'>
            {features.map((feature, i) => (
              <PricingFeature key={i} feature={feature} />
            ))}
          </ul>
        </CardContent>

        <CardFooter className='mt-auto flex-shrink-0 border-t pt-6'>
          <Button
            className='w-full'
            variant={cta.variant ?? (isFeatured ? 'default' : 'outline')}
            asChild
          >
            <Link href={cta.href}>{cta.text}</Link>
          </Button>
        </CardFooter>
      </Card>
    );
  }
);
PricingCard.displayName = 'PricingCard';

export { PricingCard, type PricingCardProps, type PricingFeature, type Price };

Usage

Import the component and provide the necessary props to display pricing tiers.

import { PricingCard } from '@/components/ui/pricing-card';

export default function PricingPage() {
  return (
    <div className='grid grid-cols-1 md:grid-cols-2 gap-8'>
      <PricingCard
        interval='month'
        name='Starter'
        description='For individuals and hobby projects.'
        price={{ monthly: 10, yearly: 100 }}
        features={[
          { text: 'Basic Feature A', included: true },
          { text: 'Basic Feature B', included: true },
          { text: 'Advanced Feature C', included: false },
        ]}
        cta={{
          text: 'Get Started',
          href: '#',
        }}
      />
      <PricingCard
        interval='month'
        name='Pro'
        description='For growing businesses and professionals.'
        price={{ monthly: 25, yearly: 250 }}
        features={[
          { text: 'Basic Feature A', included: true },
          { text: 'Basic Feature B', included: true },
          { text: 'Advanced Feature C', included: true, isNew: true },
        ]}
        cta={{
          text: 'Upgrade to Pro',
          href: '#',
        }}
        isFeatured={true}
        badgeText='Popular'
      />
    </div>
  );
}

Props

PropTypeDefaultDescription
interval'month' | 'year'ReqThe currently selected billing interval.
namestringReqThe name of the pricing plan.
descriptionstringReqA short description of the plan.
pricePrice | 'Custom'ReqThe price, either a structured object for monthly/yearly costs or 'Custom' for enterprise plans.
priceSubtitleReact.ReactNode-An optional subtitle for the price, perfect for per-seat costs or other contextual info.
featuresPricingFeature[]ReqA list of features included in the plan.
cta{ text: string; href: string; variant?: VariantProps<...>; }ReqConfiguration for the call-to-action button.
badgeTextstring-Optional text for a badge, like 'Most Popular'.
isFeaturedbooleanfalseIf true, the card will be visually highlighted as the featured plan.