Three plans with a monthly and yearly switch. The recommended plan takes the one emphasised surface a view gets. Renders in any Satori UI design system (cueit, skita, folio or a generated one): install a system first.
design-systems/blocks/pricing-01.tsx
"use client";
import * as React from "react";
import { Check } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { cn } from "@/lib/utils";
/**
* Three plans with a monthly / yearly switch. The recommended plan takes
* the emphasised surface, which every system reserves for one thing per
* view, so it is the only plan that stands out.
*
* Written against the block contract only, so it takes the look of
* whichever design system scopes it. Pass your own `plans`; prices are
* per month, and yearly shows the monthly equivalent.
*/
export type Plan = {
name: string;
description: string;
monthly: number | null;
yearly: number | null;
cta: string;
href: string;
features: string[];
recommended?: boolean;
};
const SAMPLE: Plan[] = [
{
name: "Starter",
description: "For a founder sending the first invoices.",
monthly: 0,
yearly: 0,
cta: "Start free",
href: "#",
features: ["Up to 3 customers", "Invoices and receipts", "Card payments", "Email support"],
},
{
name: "Growth",
description: "For a team that bills every week.",
monthly: 49,
yearly: 39,
cta: "Start 14-day trial",
href: "#",
recommended: true,
features: ["Unlimited customers", "Recurring billing", "Dunning and reminders", "Five seats", "Priority support"],
},
{
name: "Scale",
description: "For finance teams with an auditor.",
monthly: null,
yearly: null,
cta: "Talk to sales",
href: "#",
features: ["Everything in Growth", "SSO and audit log", "Custom contracts", "Unlimited seats", "A named contact"],
},
];
export type Pricing01Props = React.ComponentProps<"section"> & {
plans?: Plan[];
/** Controlled billing period. Leave unset to let the block hold it. */
period?: "monthly" | "yearly";
defaultPeriod?: "monthly" | "yearly";
onPeriodChange?: (period: "monthly" | "yearly") => void;
};
export function Pricing01({
plans = SAMPLE,
period: periodProp,
defaultPeriod = "yearly",
onPeriodChange,
className,
...props
}: Pricing01Props) {
const [own, setOwn] = React.useState(defaultPeriod);
const period = periodProp ?? own;
const setPeriod = (p: "monthly" | "yearly") => {
if (periodProp === undefined) setOwn(p);
onPeriodChange?.(p);
};
return (
<section className={cn("@container/pricing w-full", className)} {...props}>
<div className="mx-auto mb-10 flex max-w-xl flex-col items-center gap-4 text-center">
<p className="ds-label">Pricing</p>
<h2 className="ds-title text-foreground">Pay for the billing you do</h2>
<p className="ds-subtitle">Every plan includes unlimited invoices. Change or cancel whenever you like.</p>
<Tabs value={period} onValueChange={(v) => setPeriod(v as "monthly" | "yearly")}>
<TabsList>
<TabsTrigger value="monthly">Monthly</TabsTrigger>
<TabsTrigger value="yearly">
Yearly
<Badge variant="outline" className="tint-success">
−20%
</Badge>
</TabsTrigger>
</TabsList>
</Tabs>
</div>
<div className="grid items-stretch gap-4 @3xl/pricing:grid-cols-3">
{plans.map((plan) => {
const price = period === "yearly" ? plan.yearly : plan.monthly;
return (
<div
key={plan.name}
className={cn(
"flex flex-col gap-6 p-6",
plan.recommended ? "ds-tile-emphasis" : "ds-tile",
)}
>
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between gap-2">
<h3 className="ds-text font-semibold">{plan.name}</h3>
{plan.recommended && <span className="ds-label">Most teams</span>}
</div>
<p className="ds-small text-muted-foreground">{plan.description}</p>
</div>
<div>
{price === null ? (
<p className="ds-stat">Custom</p>
) : (
<p className="flex items-baseline gap-1.5">
<span className="ds-stat">${price}</span>
<span className="ds-small text-muted-foreground">per seat / month</span>
</p>
)}
<p className="ds-meta mt-1">
{price === null
? "Annual contract"
: price === 0
? "Free for ever"
: period === "yearly"
? `$${price * 12} billed yearly`
: "Billed monthly"}
</p>
</div>
<Button asChild variant={plan.recommended ? "secondary" : "outline"} className="w-full">
<a href={plan.href}>{plan.cta}</a>
</Button>
<ul className="flex flex-col gap-2.5">
{plan.features.map((f) => (
<li key={f} className="ds-small flex items-start gap-2.5">
<Check
aria-hidden="true"
className={cn("mt-0.5 size-4 shrink-0", !plan.recommended && "ds-ink")}
/>
{f}
</li>
))}
</ul>
</div>
);
})}
</div>
</section>
);
}