# Subscription

> A billing page in one card: the plan with a ring through its billing period, usage, card and invoices, and a cancel that offers a pause first.

Most billing pages are a plan name, a link to the provider’s portal and a cancel button that ends it in one click. Here the plan sits on a lit card with a ring that fills through the billing period and counts the days left in its middle, and a status that morphs between Renews, Trial ends, Paused until and Ends. Under it usage meters roll and turn amber near the limit, the card on file opens your provider’s page to update, and the latest invoices show their status. Cancel plan turns the lower half into “Pause instead?”: pick one, two or three months and the date billing resumes rolls to match. Only past that comes the reason and Cancel subscription, which says how long you keep the plan. A paused or ending plan offers Resume in place. The billing is yours: status and dates in, onPause, onCancel, onResume and onUpdateCard out.

## Install

```bash
npx shadcn@latest add https://hairlineui.com/r/subscription.json
```

Needs React 19, Tailwind CSS v4 and shadcn/ui theme variables (any style, light and dark). Icons from lucide-react.

- npm: `lucide-react`
- Hairline UI components it needs (the CLI installs them too): `number-roll`, `segmented-control`, `status-button`, `text-morph`, `textarea`
- Docs and live preview: https://hairlineui.com/ui/subscription
- Registry item (source): https://hairlineui.com/r/subscription.json

## Usage

```tsx
import { Subscription } from "@/components/subscription";

<Subscription
  plan={{ name: "Pro", price: 24, interval: "month" }}
  status={sub.status}
  periodStart={sub.periodStart}
  periodEnd={sub.periodEnd}
  onPause={(months) => api.pause(months)}
  onCancel={(reason, comment) => api.cancel(reason, comment)}
  onResume={() => api.resume()}
/>
```

## With your own API

Read the subscription from your server, call it for each action, and refresh. Open your provider’s portal from onUpdateCard.

```tsx
"use client";
import { useState } from "react";
import { Subscription, type SubscriptionProps } from "@/components/subscription";

type Sub = Pick<SubscriptionProps, "plan" | "status" | "periodStart" | "periodEnd" | "resumesAt" | "usage" | "card" | "invoices">;

async function post(path: string, body?: unknown): Promise<Sub> {
  const res = await fetch(path, { method: "POST", body: JSON.stringify(body ?? {}) });
  const json = await res.json();
  if (!res.ok) throw new Error(json.message);
  return json;
}

export function Billing({ initial }: { initial: Sub }) {
  const [sub, setSub] = useState(initial);

  return (
    <Subscription
      {...sub}
      onChangePlan={() => (window.location.href = "/pricing")}
      onUpdateCard={async () => {
        const { url } = await (await fetch("/api/billing/portal", { method: "POST" })).json();
        window.location.href = url;
      }}
      onPause={async (months) => setSub(await post("/api/billing/pause", { months }))}
      onCancel={async (reason, comment) => setSub(await post("/api/billing/cancel", { reason, comment }))}
      onResume={async () => setSub(await post("/api/billing/resume"))}
    />
  );
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `plan` | `{ name, price, interval: "month" \| "year", seats? }` | – | The plan on the lit card. |
| `status` | `"active" \| "trialing" \| "past_due" \| "paused" \| "canceling"` | – | Sets the status line and which actions show. |
| `periodStart / periodEnd / resumesAt` | `Date \| string \| number` | – | The billing period (the ring and the renew or end date) and, when paused, when billing resumes. |
| `usage` | `SubscriptionUsage[]` | – | { id, label, used, limit, unit? } meters. |
| `card / invoices` | `SubscriptionCard / SubscriptionInvoice[]` | – | { brand, last4, expMonth, expYear } and { id, date, amount, status, url? }; the latest three invoices show. |
| `onPause / onCancel / onResume` | `(months) / (reason, comment) / () => Promise<unknown>` | – | Change the subscription; throw an Error to show its message. Without onPause, Cancel plan goes straight to the reasons. |
| `onUpdateCard / onChangePlan` | `() => Promise<unknown> / () => void` | – | Open your portal for the card, and go to your plans. |
| `pauseMonths / reasons` | `number[] / string[]` | `[1, 2, 3] / five common reasons` | The pause lengths offered and the reasons to pick from. |
| `currency / locale` | `string` | `"USD" / browser` | Formats prices and dates. |

## States

- **active**: The lit plan card (name, seats, price, a ring with the days left and “Renews Nov 8”), then usage meters, the card on file, three invoices, Change plan and Cancel plan.
- **usage**: Meters fill from zero on mount (900ms); a meter at 90% or more turns amber with its count in full weight.
- **pause offer**: Cancel plan turns the lower half to “Pause instead?” in 460ms; the month choice is a segmented control and “Billing resumes on …” morphs with it.
- **cancel**: “No, cancel it” turns to the reasons (radio chips) and a note; Cancel subscription goes Cancelling → Cancelled, then the card says Ends and loses some colour.
- **paused**: The status reads Paused until with the date, the ring is full and dimmed, and Resume now replaces Change plan.
- **past due**: Payment failed in amber on the card, the failed invoice marked, and Update on the card opens your portal.

## What it handles

A billing page is usually a plan card, a few progress bars, a portal link and a cancel modal, with no chance to keep the customer. This block shows where they are in the period and offers a pause before the cancel.

- **Where the period stands**: A ring fills from periodStart to periodEnd with the days left inside; the status says when it renews, pauses or ends.
- **Usage that warns**: Each meter rolls its count and turns amber from 90% of the limit.
- **Card and invoices**: The card on file with Update (your portal) and the latest invoices with their status and a download.
- **A pause before the cancel**: Cancel plan offers one to three months’ pause with the date billing resumes; onPause gets the months.
- **A reason you can use**: Cancelling asks why (your reasons) with an optional note; onCancel gets both.
- **Undo in place**: Paused or ending plans offer Resume now or Keep my plan through onResume.

## Replaces

a plan card, usage meters, a payment method row, an invoice list, a cancel modal, a pause offer, a cancellation survey

## Where it fits

- **Billing settings**: The plan, usage, card and invoices on a SaaS account’s billing page.
- **Cancel flows**: A pause offered before cancelling, with the reason kept for you.
- **Dunning**: A failed payment shown on the plan with a way to update the card.
- **Memberships**: A gym, course or community membership people can pause.

## Notes

- The status is a status region, so a pause or a cancel is announced once; usage bars are meters with their numbers.
- The pause offer and the reasons are labelled groups that take focus when the surface turns to them; Keep my plan goes back.
- Dates are written once mounted, in the visitor’s zone, so the server render never disagrees.
- Its light comes from your theme: the plan card mixes --primary-foreground into --primary, the ring and its ticks are 3px and hairline strokes in the same light, and an ending plan drains some colour. No drop shadows, glow or blur.
- Built from Segmented control, Textarea, Number roll, Status button and Text morph, which the CLI installs with it. No animation library; with reduced motion the faces change in place.

License: MIT (https://hairlineui.com/license#components)
