# Status strip

> A headline number, a segmented bar and a legend for the stages of one whole, with a slot for the action that acts on it.

```tsx
"use client"

import { StatusStrip } from "@/components/ui/status-strip"
import { Button } from "@/components/ui/button"

// Fictional data. Nothing here is saved anywhere.
export function StatusStripDemo() {
  return (
    <StatusStrip
      className="w-full max-w-xl"
      headlineNoun="staff trained"
      segments={[
        { key: "done", label: "completed", count: 11, tone: "done" },
        {
          key: "active",
          label: "signed in, not trained",
          count: 4,
          tone: "active",
        },
        {
          key: "pending",
          label: "not signed in yet",
          count: 3,
          tone: "pending",
        },
      ]}
      action={<Button variant="outline">Email this summary</Button>}
    />
  )
}
```

Use it at the top of any screen that reports how far a job has got: training completion, an import run, a sync. It says "11 of 18 staff trained" once, then shows the same numbers as a bar, so the total is the sentence and not a separate tile.

Put the button that acts on the status in the `action` slot. The action then sits beside the numbers it will affect.

## Installation

**Command**

```bash
npx shadcn@latest add https://realgood.site/r/status-strip.json
```

Or, with the [`@crisp` namespace](https://realgood.site/docs/installation.md) set up:

```bash
npx shadcn@latest add @crisp/status-strip
```

It has no dependencies beyond the `cn` helper that `shadcn init` already adds.

**Manual**

**Step 1.** Copy and paste the following code into your project.

```tsx title="components/ui/status-strip.tsx"
import * as React from "react"

import { cn } from "@/lib/utils"

export type StatusTone = "done" | "active" | "pending"

export interface StatusSegment {
  /** React key. Unique within the strip. */
  key: string
  /** Shown in the legend and the bar's accessible label. */
  label: string
  /** How many fall in this stage. Sizes the bar. */
  count: number
  /** done = solid, active = tinted, pending = hatched (not started or unreachable). */
  tone: StatusTone
}

const TONE_BAR: Record<StatusTone, string> = {
  done: "bg-primary",
  active: "bg-primary/45 ring-1 ring-inset ring-primary/70",
  pending:
    "bg-[repeating-linear-gradient(135deg,currentColor_0_3px,transparent_3px_6px)] text-muted-foreground/60 ring-1 ring-inset ring-muted-foreground/30",
}

const TONE_DOT: Record<StatusTone, string> = {
  done: "bg-primary",
  active: "bg-primary/45 ring-1 ring-inset ring-primary/70",
  pending: "ring-1 ring-muted-foreground/50",
}

export interface StatusStripProps extends React.ComponentProps<"div"> {
  /** Stages of ONE whole. They must not overlap, and their counts sum to the total. */
  segments: StatusSegment[]
  /** Completes the headline sentence: "11 of 18 <noun>". Segments with tone "done" are counted. */
  headlineNoun?: string
  /** Rendered on the right of the headline: the action that acts on this status. */
  action?: React.ReactNode
}

function StatusStrip({
  segments,
  headlineNoun = "done",
  action,
  className,
  ...props
}: StatusStripProps) {
  const total = segments.reduce((sum, s) => sum + s.count, 0)
  const done = segments
    .filter((s) => s.tone === "done")
    .reduce((sum, s) => sum + s.count, 0)

  return (
    <div
      data-slot="status-strip"
      className={cn("space-y-4", className)}
      {...props}
    >
      <div className="flex flex-wrap items-end justify-between gap-x-6 gap-y-3">
        <p
          role="status"
          className="text-3xl leading-none font-semibold tracking-tight text-foreground tabular-nums"
        >
          {done}
          <span className="ml-1.5 text-sm font-normal tracking-normal text-muted-foreground">
            {" "}
            of {total} {headlineNoun}
          </span>
        </p>
        {action ? (
          <div className="flex flex-wrap items-center gap-3">{action}</div>
        ) : null}
      </div>
      {total > 0 && (
        <div
          role="img"
          aria-label={segments.map((s) => `${s.count} ${s.label}`).join(", ")}
          className="flex h-2.5 gap-[3px]"
        >
          {segments
            .filter((s) => s.count > 0)
            .map((s) => (
              <span
                key={s.key}
                className={cn("rounded-[3px]", TONE_BAR[s.tone])}
                style={{ flex: s.count }}
              />
            ))}
        </div>
      )}
      <ul
        role="list"
        className="flex flex-wrap gap-x-5 gap-y-1 text-sm text-muted-foreground tabular-nums"
      >
        {segments.map((s) => (
          <li key={s.key} className="flex items-center gap-1.5">
            <span
              className={cn("size-2 rounded-[2px]", TONE_DOT[s.tone])}
              aria-hidden="true"
            />
            <span className="font-semibold text-foreground">{s.count}</span>
            <span>{s.label}</span>
          </li>
        ))}
      </ul>
    </div>
  )
}

export { StatusStrip }
```

**Step 2.** Update the import paths to match your project setup.

## Usage

```tsx
import { StatusStrip } from "@/components/ui/status-strip"
```

```tsx
<StatusStrip
  headlineNoun="staff trained"
  segments={[
    { key: "done", label: "completed", count: 11, tone: "done" },
    {
      key: "active",
      label: "signed in, not trained",
      count: 4,
      tone: "active",
    },
    { key: "pending", label: "not signed in yet", count: 3, tone: "pending" },
  ]}
  action={<Button>Email this summary</Button>}
/>
```

## Notes

- **Segments are stages of one whole.** They must not overlap. The total is their sum, so a person who is in two segments is counted twice.
- **Tones.** `done` is solid, `active` is tinted, and `pending` is hatched, which reads as "not started or unreachable". The headline counts only `done` segments, so with more than one `done` segment they are added together.
- **Zero counts.** A segment with a count of 0 is left out of the bar but still appears in the legend. If the total is 0 there is no bar.
- **Accessibility.** The bar is an image with a label that lists every segment, for example "11 completed, 4 signed in, not trained, 3 not signed in yet". The legend is a list.
- **Colour.** The tones use `primary` and `muted-foreground`, so they follow your theme.

## What it does not do

- **It does not fetch, count or store anything.** It draws the numbers you pass. Putting each item into a segment is your code.
- **It does not stop segments overlapping.** A person in two segments is counted twice, so make every item fall in exactly one.
- **It does not act.** The `action` slot only holds the button you give it. Sending, saving and permission checks are yours.
- **It shows one moment, not a trend.** It is one whole split into stages. It is not a chart over time.
- **It makes no compliance claim.** "11 of 18 staff trained" is a display of numbers you supply, and a UI cue, not compliance. It proves nothing about who was trained.

## Props

### StatusStrip

Also accepts the props of a `div`, including `className` and `ref`.

| Prop           | Type              | Description                                                                                          |
| -------------- | ----------------- | ---------------------------------------------------------------------------------------------------- |
| `segments`     | `StatusSegment[]` | Stages of one whole. Required.                                                                       |
| `headlineNoun` | `string`          | Completes the headline: "11 of 18 `<noun>`". Default `"done"`.                                       |
| `action`       | `ReactNode`       | Rendered on the right of the headline, wrapping below it on narrow screens. Omitted when not passed. |

### StatusSegment

Exported from the same file, along with `StatusTone` and `StatusStripProps`.

| Field   | Type                              | Description                                   |
| ------- | --------------------------------- | --------------------------------------------- |
| `key`   | `string`                          | React key. Must be unique within the strip.   |
| `label` | `string`                          | Shown in the legend and the bar's label.      |
| `count` | `number`                          | How many fall in this stage. Sizes the bar.   |
| `tone`  | `"done" \| "active" \| "pending"` | `StatusTone`. Sets the fill and the headline. |

## Agent prompt

Paste this into your coding agent (Claude Code, Cursor, Codex or similar) in your project. Replace the bracketed parts with your own.

```text
Goal: add a status strip to the top of my [screen]: a headline like "11 of 18 [things] [done]", a segmented bar, a legend, and my [action, e.g. "Email this summary"] button beside the numbers.

Install: npx shadcn@latest add https://realgood.site/r/status-strip.json
Read the installed files (components/ui/status-strip.tsx) before writing any code. Do not guess props.

Contract:
type StatusSegment = { key: string; label: string; count: number; tone: "done" | "active" | "pending" }
<StatusStrip segments={StatusSegment[]} headlineNoun?={string, default "done"} action?={ReactNode} />  // also takes div props, e.g. className

Wiring rule: segments are stages of ONE whole and must not overlap, so put every item in exactly one segment and count them from my own data. The headline adds up the "done" segments only; the bar total is the sum of all counts. The component fetches and stores nothing. Put the button that acts on this status in `action`.

States to handle: loading (do not draw zeros as if they were real), a total of 0 (no bar), a segment with count 0 (left out of the bar, still in the legend), a narrow screen (the action wraps under the headline).

Acceptance checks (run them and show me the output):
1. Typecheck and lint pass.
2. A test shows the segment counts add up to the number of items I load, and no item is counted twice.
3. The headline reads "N of M <noun>", with N the sum of the done segments.
4. The bar has an accessible label that lists every segment with its count.
5. The three tones look different without colour (solid, tinted, hatched).

UI only: it does not fetch, store, send or check permissions, and it makes no compliance claim. No new dependencies or abstractions beyond this. If something is unclear, ask me.
```

## Examples by sector

Example data only, to show the wording. These are not claims about any real organisation, and the component proves nothing about a sector's rules. The pattern is the same in each; only the data changes.

- **Education.** Staff training completion: "11 of 18 staff trained", with segments completed, signed in but not trained, and not signed in yet.
- **Manufacturing.** Calibrations in date: "6 of 18 gauges in calibration", with segments in date, due in 30 days and overdue.
- **Engineering.** Engineering change orders reviewed: "7 of 12 ECOs reviewed", with segments reviewed, in review and not started.
- **Health.** Staff licences current: "41 of 46 licences current", with segments current, expiring in 60 days and expired.

## Next

Comes after: none, this is where a screen starts. Leads to: [`data-table`](https://realgood.site/docs/components/data-table.md), [`recipient-roster`](https://realgood.site/docs/components/recipient-roster.md).
