# Checkout

> A one-page checkout whose steps stack into a receipt: contact, address, delivery and your payment element, then Place order turns into the order number.

Most checkouts are four pages or one long form, and you lose track of what you entered. Here contact, shipping address, delivery and payment sit on one surface with only the step you’re on open. Finish a step and it folds into one line under its title (“Iris Okafor, 12 Elm Street, Leeds, United Kingdom · Change”) while a check draws itself in its number and a rail of light runs down to the next. Delivery speeds show the day they arrive, and the total on the lit summary rolls when you switch. Payment is your provider’s own element in the payment slot (Stripe’s Payment Element, PayPal buttons), so the block never sees card data. Place order goes Placing order → your order number. onPlaceOrder gets the whole order; that’s the only function you write.

## Install

```bash
npx shadcn@latest add https://hairlineui.com/r/checkout.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): `input`, `number-roll`, `select`, `text-morph`, `status-button`
- Docs and live preview: https://hairlineui.com/ui/checkout
- Registry item (source): https://hairlineui.com/r/checkout.json

## Usage

```tsx
import { Checkout } from "@/components/checkout";

<Checkout
  lines={lines}
  deliveries={deliveries}
  payment={<YourPaymentElement />}
  onPlaceOrder={(order) => api.placeOrder(order)} // resolve with { number }
/>
```

## With your own API

Post the order to your server, confirm the payment there, and return its number. Put whatever your payment provider renders in the payment slot.

```tsx
"use client";
import { Checkout, type CheckoutLine } from "@/components/checkout";

const deliveries = [
  { id: "standard", label: "Standard", detail: "Tracked", price: 0, days: [3, 5] as [number, number] },
  { id: "express", label: "Express", detail: "Next working day", price: 12, days: 1 },
];

export function CheckoutPage({ lines, payment }: { lines: CheckoutLine[]; payment: React.ReactNode }) {
  return (
    <Checkout
      lines={lines}
      deliveries={deliveries}
      taxRate={0.2}
      payment={payment}
      onPlaceOrder={async (order) => {
        const res = await fetch("/api/orders", { method: "POST", body: JSON.stringify(order) });
        const json = await res.json();
        if (!res.ok) throw new Error(json.message); // "Your card was declined."
        return { number: json.number };
      }}
    />
  );
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `lines` | `CheckoutLine[]` | – | { id, name, detail?, quantity, price, image?, icon? }: what’s being bought, on the summary. |
| `deliveries` | `CheckoutDelivery[]` | – | { id, label, detail?, price, days }: days from today, a number or [earliest, latest], shown as an arrival date. |
| `onPlaceOrder` | `(order: CheckoutOrder) => Promise<unknown>` | – | Gets { email, address, delivery, lines, subtotal, discount, shipping, tax, total }. Resolve with { number } to show it; throw an Error to say why not. |
| `payment` | `ReactNode` | – | Your provider’s element, shown in the payment step. |
| `discount / taxRate` | `number` | `0 / 0` | An amount off the subtotal, and tax on goods and delivery (0.2 for 20%). |
| `countries` | `{ value, label }[]` | – | The country list; defaults to a short list. |
| `defaultEmail / defaultAddress` | `string / Partial<CheckoutAddress>` | – | Prefill for signed-in customers. |
| `currency / locale` | `string` | `"USD" / browser` | Formats amounts and arrival dates. |

## States

- **contact**: The first step open with an email field and Continue; the rest wait as titles, and the lit summary shows the lines and the total.
- **step done**: Continue checks the step; its body folds closed (440ms), its one-line summary opens under the title, a check draws in its number and the rail of light runs down (620ms).
- **shipping**: Name, address, apartment, city, postcode and country (a Select); missing fields shake and say Required.
- **delivery**: Speeds as a radio list with arrival dates (“Arrives Thu, Oct 23”) and price; picking one rolls Delivery and the total.
- **payment**: Your provider’s element in the slot; Place order on the summary is enabled once the first three steps are done.
- **placed**: Place order → Placing order → “Order HL-2048” with a drawn check; the steps lock and the note says where the receipt went.
- **declined**: If onPlaceOrder throws, the button shakes into Try again and the message replaces the note.

## What it handles

A checkout is usually a stepper, three forms, a delivery picker and an order summary that each keep their own state. This block keeps them in one place and leaves payment to your provider.

- **One surface, four steps**: Only the step you’re on is open; finished ones fold into a line with Change, so nothing scrolls away.
- **Checked as you go**: Email and the required address fields are checked when you continue, and each error sits on its field.
- **Delivery with dates**: Each speed shows the day it arrives from its days, and the total rolls when you switch.
- **Your payment provider**: Put its element in the payment slot; the block never touches card numbers.
- **Totals worked out**: Lines, discount, delivery and tax (taxRate) add up on the lit summary; every change rolls.
- **The order number in place**: onPlaceOrder resolves with { number } and the button becomes “Order HL-2048”; a refusal shakes and says why.

## Replaces

a multi-step checkout, an address form, a delivery picker, an order summary, a place order button, a confirmation page

## Where it fits

- **Store checkout**: The page after the cart, with your provider’s payment element inside it.
- **One-product sales**: A product or course sold from its own page without a hosted checkout.
- **Pre-orders**: Address and delivery taken now, payment captured when it ships.
- **B2B orders**: A shipping address and delivery speed for a reorder, paid on invoice.

## Notes

- Steps are an ordered list; each finished one keeps a Change button, and focus moves into the next step when one is saved.
- Delivery speeds are a radio group with arrow keys; the country is the Select component, never the browser’s own dropdown.
- Saved steps and the placed order are announced from a status region; a refusal is an alert.
- Arrival dates are worked out once mounted, so the server render never shows a wrong day.
- Its light comes from your theme: the summary mixes --primary-foreground into --primary, line tiles take tints from --chart-1 to --chart-5, 1px edges throughout. No drop shadows, glow or blur.
- Built from Input, Select, Number roll, Status button and Text morph, which the CLI installs with it. No animation library; with reduced motion steps open and close in place.

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