# Pricing Cards

> Three-tier pricing cards with a monthly / yearly switch and a highlighted plan.

- Author: Atelier UI
- License: MIT
- Page: https://chromotion.dev/en/components/pricing-cards
- Site: Atelier UI (https://chromotion.dev)

## Install

```bash
npx shadcn@latest add https://chromotion.dev/r/pricing-cards.json
```

Stack: React + TypeScript + Tailwind CSS v4, shadcn/ui conventions. `cn` comes from `@/lib/utils`.

## Dependencies

```bash
npm install lucide-react
```

## Usage

```tsx title="components/pricing-cards-demo.tsx"
import { PricingCards } from "@/components/pricing-cards"

export default function PricingCardsDemo() {
  return (
    <PricingCards
      plans={[
        {
          name: "Hobby",
          description: "For personal projects",
          monthly: 0,
          yearly: 0,
          cta: "Get started",
          features: ["3 projects", "Community support", "Basic analytics"],
        },
        {
          name: "Pro",
          description: "For growing teams",
          monthly: 20,
          yearly: 16,
          cta: "Start free trial",
          highlighted: true,
          features: ["Unlimited projects", "Priority support", "Advanced analytics", "Custom domains"],
        },
        {
          name: "Enterprise",
          description: "For large organizations",
          monthly: 60,
          yearly: 48,
          cta: "Contact sales",
          features: ["Everything in Pro", "SSO & SAML", "Audit logs", "Dedicated manager"],
        },
      ]}
    />
  )
}
```

## Source

```tsx title="components/pricing-cards.tsx"
"use client"

import * as React from "react"
import { Check } from "lucide-react"

import { cn } from "@/lib/utils"

export type PricingPlan = {
  name: string
  description: string
  monthly: number
  yearly: number
  features: string[]
  cta: string
  highlighted?: boolean
}

export interface PricingCardsProps extends React.ComponentProps<"section"> {
  plans: PricingPlan[]
  currency?: string
  /** Label shown next to the yearly option, e.g. "-20%" */
  yearlyBadge?: string
  labels?: { monthly: string; yearly: string; perMonth: string; popular: string }
  onSelectPlan?: (plan: PricingPlan, billing: "monthly" | "yearly") => void
}

const defaultLabels = { monthly: "Monthly", yearly: "Yearly", perMonth: "/mo", popular: "Most popular" }

export function PricingCards({
  plans,
  currency = "$",
  yearlyBadge = "-20%",
  labels = defaultLabels,
  onSelectPlan,
  className,
  ...props
}: PricingCardsProps) {
  const [billing, setBilling] = React.useState<"monthly" | "yearly">("monthly")

  return (
    <section className={cn("bg-background px-6 py-16 text-foreground", className)} {...props}>
      <div className="mx-auto flex max-w-5xl flex-col items-center">
        <div role="radiogroup" className="mb-10 inline-flex rounded-full border bg-muted p-1 text-sm">
          {(["monthly", "yearly"] as const).map((option) => (
            <button
              key={option}
              role="radio"
              aria-checked={billing === option}
              onClick={() => setBilling(option)}
              className={cn(
                "inline-flex items-center gap-1.5 rounded-full px-4 py-1.5 font-medium transition-colors",
                billing === option
                  ? "bg-background text-foreground shadow-sm"
                  : "text-muted-foreground hover:text-foreground"
              )}
            >
              {labels[option]}
              {option === "yearly" && yearlyBadge && (
                <span className="rounded-full bg-emerald-500/15 px-1.5 text-xs text-emerald-600 dark:text-emerald-400">
                  {yearlyBadge}
                </span>
              )}
            </button>
          ))}
        </div>

        <div className="grid w-full gap-4 md:grid-cols-3">
          {plans.map((plan) => {
            const price = billing === "monthly" ? plan.monthly : plan.yearly
            return (
              <article
                key={plan.name}
                className={cn(
                  "relative flex flex-col rounded-2xl border bg-card p-6 text-card-foreground",
                  plan.highlighted && "border-foreground/20 shadow-xl ring-1 ring-foreground/10 md:-my-2 md:py-8"
                )}
              >
                {plan.highlighted && (
                  <span className="absolute -top-3 left-6 rounded-full bg-foreground px-2.5 py-0.5 text-xs font-medium text-background">
                    {labels.popular}
                  </span>
                )}
                <h3 className="font-medium">{plan.name}</h3>
                <p className="mt-1 text-sm text-muted-foreground">{plan.description}</p>
                <p className="mt-6 flex items-baseline gap-1">
                  <span className="text-4xl font-semibold tracking-tight tabular-nums">
                    {currency}
                    {price}
                  </span>
                  <span className="text-sm text-muted-foreground">{labels.perMonth}</span>
                </p>
                <button
                  onClick={() => onSelectPlan?.(plan, billing)}
                  className={cn(
                    "mt-6 h-10 rounded-lg text-sm font-medium transition-opacity",
                    plan.highlighted
                      ? "bg-foreground text-background hover:opacity-90"
                      : "border bg-background hover:bg-muted"
                  )}
                >
                  {plan.cta}
                </button>
                <ul className="mt-6 space-y-2.5 text-sm">
                  {plan.features.map((feature) => (
                    <li key={feature} className="flex gap-2">
                      <Check className="mt-0.5 size-4 shrink-0 text-emerald-500" />
                      <span className="text-muted-foreground">{feature}</span>
                    </li>
                  ))}
                </ul>
              </article>
            )
          })}
        </div>
      </div>
    </section>
  )
}
```
