Skip to content
Caseroom

Blocks · Marketing

Pricing

Plans side by side; the featured plan inverts, an enterprise plan can run as a wide row under them.

19% of the 2,062 sites in The UI Index have a pricing (382 sites).

Simple pricing

Start free. Upgrade when your team needs more.

  • Hobby

    €0/month

    For side projects.

    • 1 project
    • Community support
    • Basic analytics
  • Pro

    Popular

    €19/month

    For a growing product.

    • Unlimited projects
    • Email support
    • Advanced analytics
    • Custom domain
  • Team

    €49/month

    For teams of up to 20.

    • Everything in Pro
    • Roles and permissions
    • Shared workspaces
    • Priority support

Enterprise

For organisations with their own security review and billing.

  • SAML SSO and SCIM
  • Audit log
  • Uptime SLA
  • Invoice billing
Talk to sales

Both previews render the same markup; the HTML tab below is that markup without a framework.

Install

React
npx shadcn@latest add https://www.themes.dev/caseroom/r/pricing.json
Astro
npx shadcn@latest add https://www.themes.dev/caseroom/r/astro/pricing.json

Or copy the files below into your project. They need clsx, tailwind-merge and the components they import, and the Caseroom theme. The HTML needs only the compiled CSS.

pricing.classes.ts
export const pricing = {
  // @container: the plans go side by side when the block is wide enough, wherever it sits.
  root: '@container bg-background px-6 py-20 sm:py-24',
  head: 'mx-auto max-w-2xl text-center',
  title: 'text-3xl font-semibold tracking-[-0.03em] text-balance sm:text-4xl',
  description: 'mt-4 text-base text-pretty text-muted-foreground',
  grid: 'mx-auto mt-12 grid max-w-5xl gap-4 @xl:grid-cols-3',
  tier: 'flex flex-col rounded-xl border bg-card p-5 text-card-foreground @4xl:p-6',
  // The featured plan is an inverted ground (class inverted, from the theme): its button, ring and states keep
  // their contrast because every token flips with it.
  featured: 'inverted border-transparent shadow-xl',
  name: 'text-sm font-medium',
  price: 'mt-4 flex items-baseline gap-1',
  amount: 'text-4xl font-semibold tracking-[-0.03em] tabular-nums',
  period: 'text-sm opacity-70',
  tierDescription: 'mt-3 text-sm opacity-75',
  features: 'mt-6 grid gap-2.5 text-sm',
  feature: 'flex gap-2.5',
  check: 'mt-0.5 size-4 shrink-0',
  cta: 'mt-8 w-full',
  // The enterprise row: one wide card under the plans for the deal without a list price. A row, not a fourth
  // column, so the plans keep their width; it stacks on narrow blocks and puts the action at the end when wide.
  enterprise:
    'mx-auto mt-4 flex max-w-5xl flex-col gap-5 rounded-xl border bg-card p-5 text-card-foreground @2xl:flex-row @2xl:items-center @2xl:justify-between @2xl:gap-8 @4xl:p-6',
  enterpriseFeatures: 'mt-4 flex flex-wrap gap-x-5 gap-y-2 text-sm',
  enterpriseCta: 'w-full shrink-0 @2xl:w-auto',
};

export type PricingTier = {
  name: string;
  price: string;
  period?: string;
  description?: string;
  features: string[];
  cta: { label: string; href: string };
  /** One tier at most: shown inverted (black on light pages, white on dark ones). */
  featured?: boolean;
  badge?: string;
};
/** The plan without a list price ("Enterprise", "Custom"), shown as a wide row under the plans. */
export type PricingEnterprise = {
  name: string;
  description?: string;
  /** A few short lines; they run in one row when there is room. */
  features?: string[];
  cta: { label: string; href: string };
};
export type PricingContent = { title: string; description?: string; tiers: PricingTier[]; enterprise?: PricingEnterprise };
pricing.tsx
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
import { pricing, type PricingContent } from './pricing.classes';

export type PricingProps = PricingContent & { className?: string };

/** Two to four plans side by side; the featured plan inverts. Prices are text, so any currency or "Custom" works;
 * the enterprise plan can sit under them as one wide row. */
export function Pricing({ title, description, tiers, enterprise, className }: PricingProps) {
  return (
    <section className={cn(pricing.root, className)}>
      <div className={pricing.head}>
        <h2 className={pricing.title}>{title}</h2>
        {description && <p className={pricing.description}>{description}</p>}
      </div>
      <ul className={pricing.grid}>
        {tiers.map((tier) => (
          <li key={tier.name} className={cn(pricing.tier, tier.featured && pricing.featured)}>
            <div className="flex items-center justify-between gap-2">
              <h3 className={pricing.name}>{tier.name}</h3>
              {tier.badge && <Badge variant={tier.featured ? 'secondary' : 'outline'}>{tier.badge}</Badge>}
            </div>
            <p className={pricing.price}>
              <span className={pricing.amount}>{tier.price}</span>
              {tier.period && <span className={pricing.period}>{tier.period}</span>}
            </p>
            {tier.description && <p className={pricing.tierDescription}>{tier.description}</p>}
            <ul className={pricing.features}>
              {tier.features.map((feature) => (
                <li key={feature} className={pricing.feature}>
                  <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className={pricing.check}>
                    <path d="M3.5 8.5l3 3 6-7" />
                  </svg>
                  {feature}
                </li>
              ))}
            </ul>
            <div className="mt-auto">
              <Button href={tier.cta.href} variant={tier.featured ? 'default' : 'outline'} className={pricing.cta}>
                {tier.cta.label}
              </Button>
            </div>
          </li>
        ))}
      </ul>
      {enterprise && (
        <div className={pricing.enterprise}>
          <div>
            <h3 className="text-base font-semibold">{enterprise.name}</h3>
            {enterprise.description && <p className="mt-1 text-sm text-pretty text-muted-foreground">{enterprise.description}</p>}
            {enterprise.features && (
              <ul className={pricing.enterpriseFeatures}>
                {enterprise.features.map((feature) => (
                  <li key={feature} className={pricing.feature}>
                    <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className={pricing.check}>
                      <path d="M3.5 8.5l3 3 6-7" />
                    </svg>
                    {feature}
                  </li>
                ))}
              </ul>
            )}
          </div>
          <Button href={enterprise.cta.href} variant="outline" className={pricing.enterpriseCta}>
            {enterprise.cta.label}
          </Button>
        </div>
      )}
    </section>
  );
}