
Plan comparison table with grouped feature rows and a per-plan CTA in the header; on small screens a plan picker shows one plan column at a time.
@hirael
registry/hirael/bases/radix/blocks/pricing-02/pricing-02.tsx
// Pricing 02 from Hirael <https://hirael.com/blocks/pricing/pricing-02>
// MIT · Mohammad Shehadeh · https://github.com/MohammadShehadeh/hirael
'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 { ToggleGroup, ToggleGroupItem } from '@/registry/hirael/bases/radix/ui/toggle-group';
import { cn } from '@/lib/utils';
const ENTER =
'animate-in fade-in slide-in-from-bottom-4 duration-500 ease-[cubic-bezier(0.22,1,0.36,1)] fill-mode-both motion-reduce:animate-none';
// Leaving display:none restarts CSS animations, so this replays whenever a column is shown.
const SWAP =
'animate-in fade-in slide-in-from-bottom-1 duration-250 ease-[cubic-bezier(0.22,1,0.36,1)] fill-mode-both motion-reduce:animate-none';
const stagger = (index: number): React.CSSProperties => ({ animationDelay: `${index * 60}ms` });
type Cell = boolean | string;
type PlanKey = 'starter' | 'growth' | 'business';
interface Plan {
key: PlanKey;
name: string;
price: string;
cta: string;
ctaVariant: 'default' | 'outline';
featured?: boolean;
}
const PLANS: readonly Plan[] = [
{ key: 'starter', name: 'Starter', price: '$0', cta: 'Start free', ctaVariant: 'outline' },
{ key: 'growth', name: 'Growth', price: '$29', cta: 'Start trial', ctaVariant: 'default', featured: true },
{ key: 'business', name: 'Business', price: '$99', cta: 'Contact sales', ctaVariant: 'outline' },
];
interface Row {
feature: string;
cells: Record<PlanKey, Cell>;
}
const GROUPS: readonly { label: string; rows: readonly Row[] }[] = [
{
label: 'Monitoring',
rows: [
{ feature: 'Uptime monitors', cells: { starter: '10', growth: '50', business: '250' } },
{ feature: 'Check interval', cells: { starter: '5 min', growth: '1 min', business: '30 sec' } },
{ feature: 'Check regions', cells: { starter: '3', growth: '12', business: '12' } },
{ feature: 'Log retention', cells: { starter: '7 days', growth: '90 days', business: '1 year' } },
],
},
{
label: 'Alerts and status pages',
rows: [
{ feature: 'Email and Slack alerts', cells: { starter: true, growth: true, business: true } },
{ feature: 'SMS and phone calls', cells: { starter: false, growth: true, business: true } },
{ feature: 'Public status pages', cells: { starter: '1', growth: '5', business: 'Unlimited' } },
{ feature: 'Status page on your domain', cells: { starter: false, growth: true, business: true } },
],
},
{
label: 'Team',
rows: [
{ feature: 'Team members', cells: { starter: '2', growth: '10', business: 'Unlimited' } },
{ feature: 'On-call schedules', cells: { starter: false, growth: true, business: true } },
{ feature: 'SSO and audit log', cells: { starter: false, growth: false, business: true } },
],
},
];
interface CellContentProps {
value: Cell;
}
const CellContent = ({ value }: CellContentProps) => {
if (value === true) {
return (
<>
<Check aria-hidden className="size-4 text-foreground" />
<span className="sr-only">Included</span>
</>
);
}
if (value === false) {
return (
<>
<Minus aria-hidden className="size-4 text-muted-foreground" />
<span className="sr-only">Not included</span>
</>
);
}
return <span className="text-sm text-foreground tabular-nums">{value}</span>;
};
const Pricing02 = () => {
const [selected, setSelected] = React.useState<PlanKey>('growth');
/** Below md only the selected plan's column is shown; from md up every column is. */
const columnClass = (plan: Plan) =>
cn(plan.key !== selected && 'hidden md:table-cell', plan.featured && 'md:bg-primary/5');
return (
<section data-slot="pricing" className="bg-background py-20 sm:py-28">
<div className="mx-auto w-full max-w-5xl px-6 md:px-10">
<div data-slot="pricing-header" className="flex flex-col gap-5">
<span className={cn(ENTER, 'text-xs text-muted-foreground uppercase')}>Compare plans</span>
<h2
style={stagger(1)}
className={cn(ENTER, 'max-w-2xl font-serif text-4xl leading-[1.04] font-medium tracking-tight sm:text-5xl')}
>
Know when your site is down before your customers do.
</h2>
<p style={stagger(2)} className={cn(ENTER, 'max-w-2xl text-base text-muted-foreground sm:text-lg')}>
Every plan checks from several regions and alerts the right person. The larger plans check more often and
keep your history longer.
</p>
</div>
<ToggleGroup
data-slot="pricing-plan-picker"
type="single"
variant="outline"
value={selected}
onValueChange={(next) => next && setSelected(next as PlanKey)}
aria-label="Plan to show"
style={stagger(3)}
className={cn(ENTER, 'mt-10 w-full md:hidden')}
>
{PLANS.map((plan) => (
<ToggleGroupItem key={plan.key} value={plan.key} className="flex-1">
{plan.name}
</ToggleGroupItem>
))}
</ToggleGroup>
<div
data-slot="pricing-table"
style={stagger(4)}
className={cn(ENTER, 'mt-4 overflow-hidden rounded-md border border-border bg-card md:mt-12')}
>
<Table className="table-fixed border-collapse text-start">
<TableHeader>
<TableRow>
<TableHead className="w-1/2 text-start align-top whitespace-normal md:w-2/5">
<span className="text-xs font-normal text-muted-foreground uppercase">Features</span>
</TableHead>
{PLANS.map((plan) => (
<TableHead key={plan.key} className={cn('h-auto text-start align-bottom', columnClass(plan))}>
<div className={cn(SWAP, 'flex flex-col gap-3 py-4')}>
<div className="flex flex-wrap items-baseline justify-between gap-x-2 gap-y-1">
<span className="text-base font-semibold tracking-[-0.01em] text-foreground">{plan.name}</span>
<span className="text-xs text-muted-foreground">
<span dir="ltr" className="text-foreground tabular-nums">
{plan.price}
</span>{' '}
/ month
</span>
</div>
<Button asChild variant={plan.ctaVariant} size="sm" className="w-full">
<a href="#">{plan.cta}</a>
</Button>
</div>
</TableHead>
))}
</TableRow>
</TableHeader>
<TableBody>
{GROUPS.map((group) => (
<React.Fragment key={group.label}>
<TableRow>
<TableCell colSpan={PLANS.length + 1}>
<span className="text-xs text-muted-foreground uppercase">{group.label}</span>
</TableCell>
</TableRow>
{group.rows.map((row) => (
<TableRow key={row.feature}>
<TableCell className="whitespace-normal">{row.feature}</TableCell>
{PLANS.map((plan) => (
<TableCell key={plan.key} className={columnClass(plan)}>
<span className={cn(SWAP, 'flex items-center')}>
<CellContent value={row.cells[plan.key]} />
</span>
</TableCell>
))}
</TableRow>
))}
</React.Fragment>
))}
</TableBody>
</Table>
</div>
</div>
</section>
);
};
export default Pricing02;




























