Skip to content
6

Pricing 2

Compare-by-feature pricing table with sticky tier header (tier name, price, CTA) and ~8 feature rows below.

Preview

Installation

npx shadcn@latest add https://hirael.com/r/pricing-02.json

Code

components/blocks/pricing-02.tsx
'use client';

import * as React from 'react';
import { Check, Minus } from 'lucide-react';

import { Button } from '@/registry/hirael/bases/radix/ui/button';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/registry/hirael/bases/radix/ui/table';
import { cn } from '@/lib/utils';

type Cell = boolean | string;

interface Row {
  feature: string;
  hobby: Cell;
  pro: Cell;
  team: Cell;
}

interface Tier {
  key: 'hobby' | 'pro' | 'team';
  name: string;
  price: string;
  cta: string;
  ctaVariant: 'default' | 'outline';
  featured?: boolean;
}

const TIERS: readonly Tier[] = [
  {
    key: 'hobby',
    name: 'Hobby',
    price: '$0',
    cta: 'Start free',
    ctaVariant: 'outline',
  },
  {
    key: 'pro',
    name: 'Pro',
    price: '$18',
    cta: 'Start trial',
    ctaVariant: 'default',
    featured: true,
  },
  {
    key: 'team',
    name: 'Team',
    price: '$48',
    cta: 'Contact',
    ctaVariant: 'outline',
  },
];

const ROWS: readonly Row[] = [
  {
    feature: 'Registry components',
    hobby: 'Up to 5',
    pro: 'Unlimited',
    team: 'Unlimited',
  },
  { feature: 'Theme presets', hobby: '3', pro: 'Unlimited', team: 'Unlimited' },
  { feature: 'Private registry mirror', hobby: false, pro: true, team: true },
  { feature: 'Shared workspace', hobby: false, pro: false, team: true },
  { feature: 'SSO & audit log', hobby: false, pro: false, team: true },
  { feature: 'Priority response', hobby: false, pro: true, team: true },
  { feature: 'SLA support', hobby: false, pro: false, team: true },
  { feature: 'Full source ownership', hobby: true, pro: true, team: true },
];

const CellContent = ({ value }: { value: Cell }) => {
  if (value === true) {
    return <Check className="size-4 text-foreground" aria-label="Included" />;
  }
  if (value === false) {
    return <Minus className="size-4 text-muted-foreground/50" aria-label="Not included" />;
  }
  return <span className="font-mono text-xs tabular-nums text-foreground">{value}</span>;
};

const Pricing02 = () => {
  return (
    <section className="bg-background py-20 sm:py-28">
      <div className="mx-auto w-full max-w-5xl px-6 md:px-10">
        <div className="flex flex-col gap-5">
          <span className="font-mono text-[10px] uppercase tracking-[0.12em] text-muted-foreground">compare plans</span>
          <h2 className="max-w-2xl font-serif text-4xl font-medium leading-[1.04] tracking-tight sm:text-5xl">
            Every feature, side by side.
          </h2>
          <p className="max-w-2xl text-base text-muted-foreground sm:text-lg">
            One table, no marketing fog. See what each tier ships before you spend a dollar.
          </p>
        </div>

        <div className="mt-12 overflow-hidden rounded-md border border-border bg-card">
          <Table className="border-collapse text-start">
            <TableHeader className="bg-card">
              <TableRow className="border-b border-border hover:bg-transparent">
                <TableHead className="w-2/5 px-5 pt-6 pb-5 align-top text-start">
                  <span className="font-mono text-[10px] uppercase tracking-[0.12em] text-muted-foreground">plan</span>
                </TableHead>
                {TIERS.map((t) => (
                  <TableHead
                    key={t.key}
                    className={cn('px-5 py-5 align-bottom text-start', t.featured && 'bg-primary/5')}
                  >
                    <div className="flex flex-col gap-3">
                      <div className="flex items-baseline justify-between gap-2">
                        <span className="text-base font-semibold tracking-[-0.01em] text-foreground">{t.name}</span>
                        <span className="font-mono text-xs tabular-nums text-muted-foreground">
                          {t.price}
                          <span className="text-muted-foreground">/mo</span>
                        </span>
                      </div>
                      <Button asChild variant={t.ctaVariant} size="sm" className="w-full">
                        <a href="#">{t.cta}</a>
                      </Button>
                    </div>
                  </TableHead>
                ))}
              </TableRow>
            </TableHeader>
            <TableBody>
              {ROWS.map((r) => (
                <TableRow key={r.feature} className="border-b border-border last:border-b-0 hover:bg-transparent">
                  <TableCell className="px-5 py-4 font-mono text-xs uppercase tracking-[0.08em] text-foreground">
                    {r.feature}
                  </TableCell>
                  {TIERS.map((t) => (
                    <TableCell key={t.key} className={cn('px-5 py-4', t.featured && 'bg-primary/5')}>
                      <CellContent value={r[t.key]} />
                    </TableCell>
                  ))}
                </TableRow>
              ))}
            </TableBody>
          </Table>
        </div>
      </div>
    </section>
  );
};

export default Pricing02;

Dependencies

shadcn registry

buttontable

npm

lucide-react