# Pricing

> 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.

A shadcn/ui block from the [@satoriui](https://satoriui.site/?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/satoriui/pricing-01).

- Page, with a live preview: https://shadcn.exchange/r/satoriui/pricing-01
- Screenshot: https://shadcn.exchange/shot/satoriui/pricing-01/light.png
- Registry item: https://satoriui.site/r/pricing-01.json
- Listed: 2026-11-04, source last changed 2026-11-04

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/satoriui/pricing-01.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `lucide-react`
- Registry items: `badge`, `button`, `tabs`

## Files

### `design-systems/blocks/pricing-01.tsx`

```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>
  );
}
```
