# Order status

> An order’s route with a parcel riding it: stops light as it passes, the arrival day rolls, and the carrier’s scans slide in.

Most order pages are a status word and a table of scans you have to read bottom up. Here the lit card carries the order’s route, Ordered → Packed → Shipped → Out → Delivered, as a hairline with a parcel riding it. When the status moves on, the parcel travels to the new stop and every stop it passes lights as it goes by; the arrival day is large, and when the date changes the weekday morphs and the day rolls. Under it the tracking number copies in one tap, the carrier’s scans list newest first and a new one slides in at the top, and until the order ships Cancel order asks once and then cancels, dimming the route. The order is yours: pass status, eta and events from your database or the carrier’s webhook.

## Install

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

## Usage

```tsx
import { OrderStatus } from "@/components/order-status";

<OrderStatus
  number={order.number}
  status={order.status} // "ordered" | "packed" | "shipped" | "out" | "delivered" | "cancelled"
  eta={order.eta}
  carrier={order.carrier}
  tracking={order.trackingNumber}
  events={order.events}
  onCancel={() => api.cancelOrder(order.id)}
/>
```

## With your own API

Read the order from your server and check again now and then (or push it over a socket); the parcel moves whenever status changes.

```tsx
"use client";
import { useEffect, useState } from "react";
import { OrderStatus, type OrderStatusProps } from "@/components/order-status";

type Order = Omit<OrderStatusProps, "onCancel">;

export function TrackOrder({ id, initial }: { id: string; initial: Order }) {
  const [order, setOrder] = useState(initial);

  useEffect(() => {
    const timer = setInterval(async () => setOrder(await (await fetch(`/api/orders/${id}`)).json()), 30000);
    return () => clearInterval(timer);
  }, [id]);

  return (
    <OrderStatus
      {...order}
      onCancel={async () => {
        const res = await fetch(`/api/orders/${id}/cancel`, { method: "POST" });
        if (!res.ok) throw new Error((await res.json()).message); // "It's already been packed."
      }}
    />
  );
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `number` | `string` | – | The order number, shown as “Order #…”. |
| `status` | `"ordered" \| "packed" \| "shipped" \| "out" \| "delivered" \| "cancelled"` | – | Where the order is; a change moves the parcel. |
| `eta` | `Date \| string \| number` | – | When it’s expected, or when it arrived once delivered. |
| `window / address` | `string` | – | “by 6pm” after the date, and where it’s going. |
| `carrier / tracking / trackingUrl` | `string` | – | The carrier’s name, the tracking number (with Copy) and a link to their page. |
| `events` | `OrderEvent[]` | – | Newest first: { id, at, title, place? }. |
| `onCancel` | `() => Promise<unknown>` | – | Shows Cancel order while ordered or packed; throw an Error to say why not. |
| `visibleEvents / locale` | `number / string` | `3 / browser` | Scans before Show all, and how dates and times are written. |

## States

- **ordered**: The lit card with the order number, a status chip, Arriving and the date large, and the route with the parcel at Ordered; Cancel order under the scans.
- **moving**: A new status sends the parcel along the hairline (1100ms) while the lit line follows; each stop lights in turn as the parcel passes and its label brightens. The chip morphs to the new stage.
- **date**: When eta changes the weekday and month morph and the day rolls.
- **scan**: A new event slides in at the top of Updates with the lit dot; older ones move down.
- **delivered**: The parcel at the last stop, every stop lit, the headline Delivered with the day it arrived.
- **cancelled**: Cancel order asks “Yes, cancel this order” (reverts after 4s), then Cancelling → Cancelled; the card loses its colour, the route dims and the date reads Not on its way.

## What it handles

An order page is usually a status badge, a progress bar, a tracking link and a list of scans that disagree with each other. This block draws them from one status and one list.

- **Where it is, at a glance**: Five stops on a route; the parcel sits at the current one and the passed ones are lit.
- **Moves you can see**: A new status sends the parcel along the route and lights each stop as it passes.
- **The day, large**: eta reads as the weekday and date; a new date morphs and rolls, delivered reads Delivered.
- **Tracking in one tap**: Carrier, tracking number with Copy, and a Track link out.
- **Scans as they come**: events newest first, three until Show all; a new one slides in at the top.
- **Cancel before it ships**: onCancel shows Cancel order while it’s ordered or packed; it asks once, then Cancelling → Cancelled.

## Replaces

an order status badge, a shipping progress bar, a tracking number field, a tracking timeline, a cancel order flow

## Where it fits

- **Order pages**: The page an order email links to, from placed to delivered.
- **Account orders**: Each order in a customer’s account, with tracking and cancel.
- **Food and grocery**: An order moving from kitchen to door, with a time instead of a date.
- **Print and hardware**: Made-to-order items through making, shipping and delivery.

## Notes

- The route is an ordered list with the current stop marked aria-current="step"; each stop says done or current to screen readers.
- The status chip is a status region, so a new stage is announced once; scans are a plain list.
- The parcel is a single composited layer moved along the route, never re-laid out per frame.
- Its light comes from your theme: the card mixes --primary-foreground into --primary, the route is 1px and 2px lines in the same light, and a cancelled order drains its colour. No drop shadows, glow or blur.
- Built from Copy button, Number roll, Status button and Text morph, which the CLI installs with it. No animation library; with reduced motion the parcel and stops change in place.

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