# Status

Three components that show the state of a record with a tone and a label.

```tsx
import { StatusBadge, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, type StatusTone } from 'ferry-ui'

type InvoiceStatus = 'paid' | 'open' | 'overdue'

// One map from the statuses of your domain to a tone and a label.
const INVOICE_STATUS: Record<InvoiceStatus, { tone: StatusTone; label: string }> = {
  paid: { tone: 'success', label: 'Paid' },
  open: { tone: 'info', label: 'Open' },
  overdue: { tone: 'destructive', label: 'Overdue' },
}

const INVOICES: { number: string; customer: string; status: InvoiceStatus }[] = [
  { number: 'INV-2041', customer: 'Acme', status: 'paid' },
  { number: 'INV-2042', customer: 'Globex', status: 'open' },
  { number: 'INV-2043', customer: 'Northwind Traders', status: 'overdue' },
]

export default function StatusHero() {
  return (
    <Table aria-label="Invoices">
      <TableHeader>
        <TableRow className="hover:bg-transparent">
          <TableHead>Invoice</TableHead>
          <TableHead>Customer</TableHead>
          <TableHead>Status</TableHead>
        </TableRow>
      </TableHeader>
      <TableBody>
        {INVOICES.map((invoice) => (
          <TableRow key={invoice.number}>
            <TableCell className="font-mono text-[13px]">{invoice.number}</TableCell>
            <TableCell>{invoice.customer}</TableCell>
            <TableCell>
              <StatusBadge {...INVOICE_STATUS[invoice.status]} />
            </TableCell>
          </TableRow>
        ))}
      </TableBody>
    </Table>
  )
}
```

## Usage guidelines

- **A state is not a tag.** Use `StatusBadge` for the state of a record. For a static tag, a plan, a version or a count, use [Badge](/docs/components/badge).
- **Map your statuses one time.** Write one map from each status of your domain to a tone and a label. Keep it next to your types.
- **Color is not the only sign.** `StatusBadge` has a label. A `StatusDot` with no text next to it needs `label`.
- **Motion shows work in progress.** Set `pulse` or `spin` only while the work runs.
- **A message with an action is a callout.** Use [Callout](/docs/components/callout).

## Anatomy

Import the components. Each component has one part.

```tsx title="Anatomy"

<StatusBadge tone="success" label="Active" />
<StatusDot tone="success" />
<StatusLine tone="success">Project is active</StatusLine>
```

| Component | Look | Place |
| --- | --- | --- |
| `StatusBadge` | An uppercase pill with a dot. | Tables, headers, tiles and cards. |
| `StatusDot` | A 6px dot. | Before a name in a list or a menu. |
| `StatusLine` | An icon in a circle, then a sentence. | Card footers and summary panels. |

## Examples

### Tones

The three components use the same `tone` prop. `STATUS_TONES` lists the tones in display order.

```tsx
import { STATUS_TONES, StatusBadge, type StatusTone } from 'ferry-ui'

const LABELS: Record<StatusTone, string> = {
  success: 'Active',
  warning: 'Past due',
  destructive: 'Failed',
  info: 'In review',
  neutral: 'Draft',
}

export default function StatusTones() {
  return (
    <>
      {STATUS_TONES.map((tone) => (
        <StatusBadge key={tone} tone={tone} label={LABELS[tone]} />
      ))}
    </>
  )
}
```

| Tone | Meaning |
| --- | --- |
| `success` | Healthy, done, active, paid. |
| `warning` | Degraded, needs attention, expiring. |
| `destructive` | Failed, overdue, blocked. |
| `info` | In progress, pending review. |
| `neutral` (default) | Inactive, paused, draft, unknown. |

### Sizes

A `StatusBadge` is 20px high. Next to a page title or a card title, use `size="sm"`. It is 18px high.

```tsx
import { StatusBadge } from 'ferry-ui'

export default function StatusSizes() {
  return (
    <div className="flex flex-col gap-4">
      <StatusBadge tone="success" label="Active" />
      <div className="flex items-center gap-2">
        <span className="text-base font-medium text-foreground">Billing portal</span>
        <StatusBadge tone="success" label="Active" size="sm" />
      </div>
    </div>
  )
}
```

### Pulse

Set `pulse` to add a pulse to the dot. Remove it when the work stops.

```tsx
import * as React from 'react'
import { Button, StatusBadge } from 'ferry-ui'

export default function StatusPulse() {
  const [running, setRunning] = React.useState(true)

  return (
    <>
      {running ? (
        <StatusBadge tone="info" label="Running" pulse />
      ) : (
        <StatusBadge tone="neutral" label="Stopped" />
      )}
      <Button size="tiny" onClick={() => setRunning((value) => !value)}>
        {running ? 'Stop the import' : 'Start the import'}
      </Button>
    </>
  )
}
```

### Icon

An `icon` replaces the dot. To show the icon and the dot, set `dot`. To hide the dot, set `dot` to `false`.

```tsx
import { StatusBadge } from 'ferry-ui'
import { CreditCard, Lock, ShieldAlert } from 'lucide-react'

export default function StatusIcon() {
  return (
    <>
      <StatusBadge tone="destructive" icon={<ShieldAlert />} label="Blocked" />
      <StatusBadge tone="neutral" icon={<Lock />} label="Private" />
      <StatusBadge tone="warning" icon={<CreditCard />} label="Card expires" dot />
      <StatusBadge tone="neutral" label="Draft" dot={false} />
    </>
  )
}
```

### Status dot

Put a `StatusDot` before text that names the state. If no text names the state, give the dot a `label`.

```tsx
import { StatusDot } from 'ferry-ui'

export default function StatusDots() {
  return (
    <ul className="flex flex-col gap-2 text-[13px] text-foreground-light">
      <li className="flex items-center gap-2">
        <StatusDot tone="success" /> 3 endpoints online
      </li>
      <li className="flex items-center gap-2">
        <StatusDot tone="info" pulse /> Live updates on
      </li>
      <li className="flex items-center gap-2">
        {/* No text names the state: the dot needs a label. */}
        <StatusDot tone="destructive" label="Offline" />
        <span className="font-mono">orders-webhook</span>
      </li>
    </ul>
  )
}
```

### Status line

A `StatusLine` shows an icon for its tone, then one line of text. With `spin`, the icon is a spinner. Use `icon` to give another icon.

```tsx
import { StatusLine } from 'ferry-ui'
import { Pause } from 'lucide-react'

export default function StatusLines() {
  return (
    <div className="flex flex-col gap-3">
      <StatusLine tone="success">Workspace is active</StatusLine>
      <StatusLine tone="warning">Usage is above 90% of the plan</StatusLine>
      <StatusLine tone="destructive">Payment failed</StatusLine>
      <StatusLine tone="info" spin>
        Import of contacts in progress
      </StatusLine>
      <StatusLine tone="neutral" icon={<Pause />}>
        Subscription is on hold
      </StatusLine>
    </div>
  )
}
```

### Badge look on another element

`statusBadgeVariants` returns the classes of the badge. Use it to give the badge look to a button or a link.

```tsx
import * as React from 'react'
import { StatusDot, cn, statusBadgeVariants, type StatusTone } from 'ferry-ui'

const FILTERS: { tone: StatusTone; label: string }[] = [
  { tone: 'success', label: 'Paid' },
  { tone: 'warning', label: 'Overdue' },
  { tone: 'destructive', label: 'Failed' },
]

export default function StatusVariants() {
  const [active, setActive] = React.useState<StatusTone | null>('success')

  return (
    <div role="group" aria-label="Filter invoices by status" className="flex flex-wrap items-center gap-2">
      {FILTERS.map(({ tone, label }) => {
        const pressed = active === tone
        return (
          <button
            key={tone}
            type="button"
            aria-pressed={pressed}
            onClick={() => setActive(pressed ? null : tone)}
            className={cn(
              statusBadgeVariants({ tone: pressed ? tone : 'neutral' }),
              'cursor-pointer outline-none focus-visible:ring-2 focus-visible:ring-ring',
            )}
          >
            <StatusDot tone={tone} />
            {label}
          </button>
        )
      })}
    </div>
  )
}
```

## Accessibility

- A `StatusDot` with no `label` is decorative. With a `label`, it is an image with this name.
- The icon of a `StatusLine` is decorative. Write the state in the text.
- The pulse and the spin stop if the user prefers reduced motion.

## API reference

The three components also accept each attribute of the `<span>` element.

### StatusBadge

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `label` (required) | `ReactNode` |  | The state, 1–3 words ("Active", "Past due", "Syncing"). Rendered uppercase; it never wraps. |
| `tone` | `"destructive" \| "warning" \| "success" \| "info" \| "neutral"` | `neutral` | Colour of the badge (and of its dot). Defaults to `neutral`. |
| `size` | `"sm" \| "md"` | `md` | Height and padding: `md` (20px, default) everywhere; `sm` (18px, 10px text) inline next to a page or card title where a 20px pill looks heavy. |
| `pulse` | `boolean` | `false` | Pulses the leading dot for live or in-progress states ("Running", "Processing"). No effect when the dot is hidden (`dot={false}`, or an `icon` without `dot`). |
| `icon` | `ReactNode` |  | Leading icon (lucide, sized to 12px). When set, the dot is hidden unless `dot` is forced to `true`. |
| `dot` | `boolean` |  | Shows the leading status dot. Defaults to `true`, or `false` when an `icon` is given. |

### StatusDot

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `tone` | `"destructive" \| "warning" \| "success" \| "info" \| "neutral"` | `neutral` | Colour of the dot. Defaults to `neutral`. |
| `pulse` | `boolean` | `false` | Adds a soft ping ring around the dot. Use it only for live or in-progress states (running, syncing, recording). |
| `label` | `string` |  | Accessible name ("Online", "Syncing"). Without it the dot is decorative (`aria-hidden`), which is right when a text label sits next to it. Set it when the dot is the only status cue (e.g. a dot-only table column). |

### StatusLine

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children` (required) | `ReactNode` |  | The sentence describing the state ("Workspace is active", "Payment failed"). Truncates on one line. |
| `tone` | `"destructive" \| "warning" \| "success" \| "info" \| "neutral"` | `neutral` | Colour of the circled icon. Defaults to `neutral`. |
| `icon` | `ReactNode` |  | Icon inside the 20px circle (sized to 12px). Defaults to a per-tone icon: check (success), triangle (warning), cross (destructive), info (info), dashed circle (neutral) — or a spinner when `spin` is set. |
| `spin` | `boolean` | `false` | Spins the icon for in-progress states ("Syncing…", "Importing…"). Without `icon`, shows a spinner. |
| `iconClassName` | `string` |  | Extra classes for the circle, e.g. a custom ring colour (`border-primary/40 text-primary`). |

### Helpers

| Name | Type | Role |
| --- | --- | --- |
| `statusBadgeVariants` | `({ tone, size }) => string` | Returns the classes of a `StatusBadge`. |
| `STATUS_TONES` | `readonly StatusTone[]` | Lists the five tones in display order. |
| `StatusTone` | type | `'success'`, `'warning'`, `'destructive'`, `'info'` or `'neutral'`. |
