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

npx shadcn@latest add https://satisui.xyz/r/pricing-card.json

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.