Skip to content
Join UI
Data DisplaynewAdded Aug 2, 2026

Status Timeline

A colour-coded step tracker for orders, deployments and onboarding, still at rest and animated only when a step advances.

View sourceon GitHub (opens in a new tab)Open full preview

StatusTimeline renders a fixed sequence of steps and marks where the process currently stands. Pass an activeStep index and it derives the rest — earlier steps turn green, the one in flight turns blue, the rest stay grey behind a dashed ring — or pin a state per step to describe a run that is waiting on someone or has failed outright. At rest it is a still drawing: nothing loops and nothing breathes. The motion is spent on the one moment worth showing — move activeStep on and the connector above the cleared step draws downward, the marker it reaches cross-fades into its new ring, and a single pulse leaves the step now in flight. It lays out vertically as a tracking card or horizontally as a wizard header, and every hue is backed by a glyph and a text label, so the state never rests on colour alone.

Preview

Status Timeline
Driven by activeStep

One index decides everything. Advance it and the line draws down, the next marker cross-fades into blue and pulses once — then everything goes still again.

DeliveryIn progress
  1. Order confirmed, Completed

    Payment captured, order placed.

    17 Nov, 13:45
  2. Packed, Completed

    Leaving the Rotterdam warehouse.

    17 Nov, 16:02
  3. In transit, In progress

    Handed to the courier.

    18 Nov, 08:20
  4. Delivered, Not started

    Signed for at the front desk.

    19 Nov, 11:07
3 / 4
Finished, with an action

Past the last index every step is complete, and the header chip goes green with it.

DeliveryCompleted
  1. Order confirmed, Completed

    Payment captured, order placed.

    17 Nov, 13:45
  2. Packed, Completed

    Leaving the Rotterdam warehouse.

    17 Nov, 16:02
  3. In transit, Completed

    Handed to the courier.

    18 Nov, 08:20
  4. Delivered, Completed

    Signed for at the front desk.

    19 Nov, 11:07
orientation="horizontal"

The same steps laid along a rule — a checkout or wizard header.

CheckoutIn progress
  1. Cart, Completed

  2. Address, In progress

  3. Payment, Not started

  4. Review, Not started

state: "waiting"

Amber for a step that is neither moving nor broken — an approval, a review, a queue.

OnboardingWaiting
  1. Account created, Completed

  2. Workspace configured, Completed

  3. Identity review, Waiting

    Usually clears within an hour.

  4. Invite your team, Not started

state: "blocked", variant="plain"

Red for a step that failed, with the card and header dropped so the list can sit in a surface you own.

  1. Install, Completed

    12s
  2. Typecheck, Completed

    31s
  3. Test, Blocked

    4 of 212 assertions failed.

    1m 04s
  4. Deploy, Not started

Installation

Install Status Timeline with the shadcn CLI. The registry item resolves its own dependencies and writes the file to components/joinui/status-timeline.tsx.

pnpm dlx shadcn@latest add @joinui/status-timeline

Usage

example.tsx
import { StatusTimeline } from "@/components/joinui/status-timeline"

export function Example() {
  return (
    <StatusTimeline
      label="Delivery"
      activeStep={2}
      steps={[
        { id: "confirmed", title: "Order confirmed", timestamp: "17 Nov, 13:45" },
        { id: "packed", title: "Packed", timestamp: "17 Nov, 16:02" },
        { id: "transit", title: "In transit", timestamp: "18 Nov, 08:20" },
        { id: "delivered", title: "Delivered" },
      ]}
    />
  )
}

Props

StatusTimeline

Props for StatusTimeline
PropTypeDefaultDescription
steps (required)StatusTimelineStep[]The sequence to render, in order.
activeStepnumber0Index of the step in flight. Earlier steps resolve to complete, later ones to pending. Pass steps.length to complete the whole sequence.
orientation"vertical" | "horizontal""vertical"Vertical stacks the steps as a tracking card; horizontal lays them along a rule as a wizard header.
size"sm" | "md""md"Marker, type and spacing scale.
labelstring"Timeline"Header eyebrow, and the accessible name of the list when the header is hidden.
statusstringOverrides the header chip copy, which is otherwise rolled up from the resolved steps.
variant"card" | "plain""card"Plain drops the surrounding rule, background and padding so the list can sit inside your own container.
showHeaderbooleantrueRenders the eyebrow and status chip above the steps.
footerReact.ReactNodeContent placed below a rule — an action, a note, a summary row.
classNamestringMerged onto the root element through `cn`.

StatusTimelineStep

Shape of a single entry in the `steps` array.

Props for StatusTimelineStep
PropTypeDefaultDescription
id (required)stringStable identity for the rendered list item.
title (required)stringStep heading.
descriptionstringSupporting line below the title.
timestampstringTrailing meta, set in mono with tabular figures — a time, a duration, an ETA.
state"complete" | "current" | "waiting" | "pending" | "blocked"Pins the state instead of deriving it from activeStep, and picks the hue: green for complete, blue for current, amber for waiting, grey for pending, red for blocked.
iconReact.ReactNodeReplaces the default state glyph inside the marker. Sized by the component, so pass a bare icon element.

Dependencies

npm packages

  • motion
  • lucide-react

Registry items

  • utils

Installed automatically by the CLI when they are missing.

Accessibility

  • Steps render as an ordered list, so assistive technology reports both position and total.
  • The step in flight carries `aria-current="step"`.
  • Colour is never the only signal, which is what satisfies WCAG 1.4.1: every state also has its own glyph — a tick, a pip, a clock, a cross — and appends a visually hidden label reading “Completed”, “In progress”, “Waiting”, “Not started” or “Blocked”.
  • A blocked step additionally shifts its title to the critical hue, so the failure is findable without reading every marker.
  • The list is named by the header eyebrow through `aria-labelledby`, or by `label` when the header is hidden.
  • Markers, trails and the arrival ring are `aria-hidden` and non-interactive; only content you pass to `footer` enters the tab order.
  • Nothing animates on mount or loops at rest, so the component never competes with the page for attention; the advance transition animates `transform` and `opacity` only, and resolves instantly under `prefers-reduced-motion`.
  • Both themes are covered by the tokens rather than by `dark:` variants, so the component keeps its contrast inside a forced-theme subtree.

Keyboard interactions

Status Timeline renders no interactive controls of its own, so it adds nothing to the tab order.

Customization

A horizontal wizard header

Drop the descriptions and switch orientation to get a checkout stepper that spans its container.

tsx
<StatusTimeline
  label="Checkout"
  orientation="horizontal"
  size="sm"
  activeStep={1}
  steps={[
    { id: "cart", title: "Cart" },
    { id: "address", title: "Address" },
    { id: "payment", title: "Payment" },
    { id: "review", title: "Review" },
  ]}
/>

Pin a step that failed, or one that is waiting

A state on the step wins over the one derived from activeStep, so a run that stalled can be described exactly. Blocked turns the marker and the title red; waiting turns the marker amber for something that is neither moving nor broken, like an approval sitting in a queue. Either way the header chip picks the state up on its own.

tsx
<StatusTimeline
  label="Pipeline"
  steps={[
    { id: "install", title: "Install", timestamp: "12s", state: "complete" },
    { id: "typecheck", title: "Typecheck", timestamp: "31s", state: "complete" },
    {
      id: "test",
      title: "Test",
      description: "4 of 212 assertions failed.",
      state: "blocked",
    },
    { id: "deploy", title: "Deploy", state: "pending" },
  ]}
/>

<StatusTimeline
  label="Onboarding"
  steps={[
    { id: "account", title: "Account created", state: "complete" },
    {
      id: "review",
      title: "Identity review",
      description: "Usually clears within an hour.",
      state: "waiting",
    },
    { id: "invite", title: "Invite your team", state: "pending" },
  ]}
/>

Give each step its own glyph

An icon replaces the default state glyph while the ring and the hidden state label keep carrying the meaning — a dashed grey circle before the step happens, a tinted one after. The component sizes it, so pass the element bare.

tsx
import { MapPin, Package, ShoppingBag, Truck } from "lucide-react"

<StatusTimeline
  activeStep={2}
  steps={[
    { id: "confirmed", title: "Order confirmed", icon: <ShoppingBag /> },
    { id: "packed", title: "Packed", icon: <Package /> },
    { id: "transit", title: "In transit", icon: <Truck /> },
    { id: "delivered", title: "Delivered", icon: <MapPin /> },
  ]}
/>

Sit inside your own container

The plain variant removes the rule, background and padding, and the header can go with it — useful inside a drawer or an existing card.

tsx
<div className="rounded-soft-lg border border-border p-6">
  <h3 className="mb-4 text-sm font-medium">Onboarding</h3>
  <StatusTimeline
    variant="plain"
    showHeader={false}
    label="Onboarding"
    size="sm"
    activeStep={1}
    steps={steps}
  />
</div>

Add an action below the steps

Anything passed to footer renders below a rule. It is the only part of the component that can take focus, so it is where a call to action belongs.

tsx
<StatusTimeline
  label="Delivery"
  activeStep={steps.length}
  steps={steps}
  footer={
    <button
      type="button"
      className="inline-flex w-full items-center justify-center gap-1.5 rounded-full bg-positive-soft px-3 py-1.5 text-xs font-medium text-positive"
    >
      <Star aria-hidden="true" className="size-3.5" />
      Rate this delivery
    </button>
  }
/>

Retint the states

The states read the component palette rather than literal colours, so a brand hue is a token override — no props to thread and no variants to add. Redeclare a family in both themes and every step, chip and trail follows.

css
/* app/globals.css */
:root,
.light {
  /* A violet in-flight state instead of the default blue. */
  --info: oklch(0.5 0.19 292);
  --info-soft: oklch(0.965 0.025 292);
  --info-foreground: oklch(0.99 0.005 292);
}

.dark {
  --info: oklch(0.76 0.15 292);
  --info-soft: oklch(0.255 0.06 292);
  --info-foreground: oklch(0.16 0.04 292);
}
  • A coil of pills that keeps one item in focus and lets the rest twist away, turned either by a timer or by the page scroll.