Skip to content
    Style: Wireframe

    Blocks · Marketing

    Pricing

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

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

    Used in UI Pro: Pricing

    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/ui/r/pricing.json
    Astro
    npx shadcn@latest add https://www.themes.dev/ui/r/astro/pricing.json

    Or copy the files below into your project. They need clsx, tailwind-merge and the components they import, and the themes.dev UI 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>
      );
    }