
Three-tier card row with a monthly/yearly toggle that swaps each price and billing note, a featured middle plan, feature checklist and CTA.
@hirael
registry/hirael/bases/radix/blocks/pricing-01/pricing-01.tsx
// Pricing 01 from Hirael <https://hirael.com/blocks/pricing/pricing-01>
// MIT · Mohammad Shehadeh · https://github.com/MohammadShehadeh/hirael
'use client';
import * as React from 'react';
import { Check } from 'lucide-react';
import { Button } from '@/registry/hirael/bases/radix/ui/button';
import { Card, CardContent, CardFooter, CardHeader } from '@/registry/hirael/bases/radix/ui/card';
import { Separator } from '@/registry/hirael/bases/radix/ui/separator';
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';
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, step = 60, offset = 0): React.CSSProperties => ({
animationDelay: `${offset + index * step}ms`,
});
type Billing = 'monthly' | 'yearly';
const PAID_MONTHS_PER_YEAR = 10;
interface Tier {
name: string;
/** Monthly price in whole dollars when billed monthly. */
monthly: number;
blurb: string;
features: readonly string[];
cta: string;
featured?: boolean;
ctaVariant: 'default' | 'outline';
}
const TIERS: readonly Tier[] = [
{
name: 'Solo',
monthly: 0,
blurb: 'For freelancers sending a handful of invoices a month.',
features: ['5 active clients', 'Invoices and quotes', 'Card and bank payments', 'Email reminders'],
cta: 'Start free',
ctaVariant: 'outline',
},
{
name: 'Studio',
monthly: 24,
blurb: 'For small studios billing retainers and project work.',
features: ['Unlimited clients', 'Recurring invoices', 'Client portal with your logo', 'Time tracking'],
cta: 'Try Studio free for 14 days',
featured: true,
ctaVariant: 'default',
},
{
name: 'Agency',
monthly: 64,
blurb: 'For agencies with several teams and a finance lead.',
features: ['Everything in Studio', 'Up to 25 team members', 'Approval before sending', 'Accounting sync'],
cta: 'Talk to us',
ctaVariant: 'outline',
},
];
const money = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 });
const priceFor = (tier: Tier, billing: Billing) =>
billing === 'yearly' ? Math.round((tier.monthly * PAID_MONTHS_PER_YEAR) / 12) : tier.monthly;
const Pricing01 = () => {
const [billing, setBilling] = React.useState<Billing>('monthly');
return (
<section data-slot="pricing" className="bg-background py-20 sm:py-28">
<div className="mx-auto w-full max-w-[1480px] px-4">
<div data-slot="pricing-header" className="mx-auto flex max-w-2xl flex-col items-center gap-5 text-center">
<span className={cn(ENTER, 'text-xs text-muted-foreground uppercase')}>Pricing</span>
<h2
style={stagger(1)}
className={cn(ENTER, 'font-serif text-4xl leading-[1.04] font-medium tracking-tight sm:text-5xl')}
>
Send invoices free. Pay when you <span className="text-foreground italic">grow</span>.
</h2>
<p style={stagger(2)} className={cn(ENTER, 'text-base text-muted-foreground sm:text-lg')}>
Every plan takes card and bank payments with no extra fee from us. Upgrade when you need more clients or
more people.
</p>
<div style={stagger(3)} className={cn(ENTER, 'mt-2 flex flex-col items-center gap-2')}>
<ToggleGroup
data-slot="pricing-billing"
type="single"
variant="outline"
value={billing}
onValueChange={(next) => next && setBilling(next as Billing)}
aria-label="Billing period"
>
<ToggleGroupItem value="monthly">Monthly</ToggleGroupItem>
<ToggleGroupItem value="yearly">Yearly</ToggleGroupItem>
</ToggleGroup>
<span className="text-xs text-muted-foreground">Yearly plans get 2 months free</span>
</div>
</div>
<div data-slot="pricing-tiers" className="mt-14 grid grid-cols-1 gap-4 md:grid-cols-3">
{TIERS.map((tier, index) => {
const price = priceFor(tier, billing);
return (
<Card
key={tier.name}
data-slot="pricing-tier"
data-featured={tier.featured ? '' : undefined}
style={{
...stagger(index, 60, 240),
...(tier.featured
? {
backgroundImage:
'radial-gradient(120% 90% at 50% 0%, color-mix(in oklch, var(--primary) 9%, transparent), transparent 60%)',
}
: undefined),
}}
className={cn(ENTER, 'relative')}
>
{tier.featured && (
<span className="absolute end-6 -top-2.5 inline-flex items-center rounded-full border border-border bg-background px-2.5 py-0.5 text-xs text-foreground uppercase">
Most popular
</span>
)}
<CardHeader className="flex flex-col">
<h3 className="text-xs font-normal text-muted-foreground uppercase">{tier.name}</h3>
<div key={billing} data-slot="pricing-price" className={cn(SWAP, 'flex flex-col gap-1')}>
<div className="flex items-baseline gap-1">
<span dir="ltr" className="text-4xl font-semibold tracking-[-0.04em] tabular-nums">
{money.format(price)}
</span>
<span className="text-xs text-muted-foreground">/ month</span>
</div>
<span className="text-xs text-muted-foreground">
{tier.monthly === 0 ? (
'Free for as long as you like'
) : billing === 'yearly' ? (
<>
<span dir="ltr" className="tabular-nums">
{money.format(tier.monthly * PAID_MONTHS_PER_YEAR)}
</span>{' '}
billed once a year
</>
) : (
'Billed monthly, cancel any time'
)}
</span>
</div>
<p className="pt-1 text-sm text-muted-foreground">{tier.blurb}</p>
</CardHeader>
<Separator />
<CardContent>
<ul className="flex flex-col gap-2.5">
{tier.features.map((feature) => (
<li key={feature} className="flex items-start gap-2.5 text-sm text-foreground">
<Check aria-hidden className="mt-0.5 size-4 shrink-0 text-foreground" />
{feature}
</li>
))}
</ul>
</CardContent>
<CardFooter className="mt-auto">
<Button asChild variant={tier.ctaVariant} size="lg" className="w-full">
<a href="#">{tier.cta}</a>
</Button>
</CardFooter>
</Card>
);
})}
</div>
</div>
</section>
);
};
export default Pricing01;



























