Step Timeline

An ordered list of steps with a "current step" cursor. Steps before the cursor render as done (filled accent marker with check), the cursor step renders as active (hollow accent marker with a subtle pulse), and the rest render as upcoming (hollow muted marker). Lays out vertically or horizontally; connectors between markers can be solid, dotted, or hidden.

  1. Mise en place
    Chop, measure, line everything up.
  2. Sear
    Get a hard crust on each side.
  3. Braise
    Low heat, lid on, 90 minutes.
  4. Plate
    Rest, slice, sauce, serve.
Customize
Active step
1
Orientation
vertical
Connector
line

Installation

npx shadcn@latest add https://craftbits.dev/r/step-timeline.json

Usage

import { StepTimeline } from "@craft-bits/core";
 
<StepTimeline
  aria-label="Onboarding progress"
  activeStep={1}
  steps={[
    { title: "Create account" },
    { title: "Verify email" },
    { title: "Add a profile picture" },
    { title: "Invite teammates" },
  ]}
/>

Horizontal layout with descriptions:

<StepTimeline
  orientation="horizontal"
  activeStep={2}
  steps={[
    { title: "Build", description: "Compile + bundle" },
    { title: "Test", description: "Unit + integration" },
    { title: "Stage", description: "Deploy to staging" },
    { title: "Ship", description: "Promote to prod" },
  ]}
/>

Per-step status (when activeStep doesn't fit your model):

<StepTimeline
  steps={[
    { title: "Draft", status: "done" },
    { title: "Review", status: "active" },
    { title: "Sign off", status: "upcoming" },
    { title: "Publish", status: "upcoming" },
  ]}
/>

Understanding the component

  1. Status derivation. When activeStep is set, each step's status is computed from its index: < activeStep → done, === activeStep → active, > activeStep → upcoming. When activeStep is omitted, per-step status wins — which lets you express non-contiguous layouts.
  2. Connector tone follows the leading step. Each <li> owns its own connector segment, positioned absolutely from below (or right of) the marker. The segment colour tracks the leading step's status — connectors emanating from done or active steps pick up border-cb-accent/60; otherwise they stay border-cb-border. This yields the canonical "filling pipeline" visual without a separate connector primitive.
  3. Staggered entry. Each step fades in with a 6px slide along the cross-axis. Per-step delay is index * STAGGER (0.04s), capped at 0.05s by ESLint — the reveal cascades but never feels like a slideshow.
  4. Active pulse. The active marker breathes via animate={{ scale: [1, 1.05, 1] }} on SPRINGS.smooth, with a 1.2s pause between cycles. The pulse stays inside the [0.95, 1.05] subtle-deformation range.
  5. Reduced motion. When prefers-reduced-motion: reduce is set, both the stagger and the active pulse short-circuit — markers render in place, no slide, no breathing.

Props

PropTypeDefaultDescription
steps{ title: string; description?: ReactNode; status?: 'done' | 'active' | 'upcoming' }[]requiredOrdered list of steps.
activeStepnumber0-indexed cursor. Overrides per-step status.
orientation'vertical' | 'horizontal''vertical'Layout axis.
connector'line' | 'dotted' | 'none''line'Connector style between markers.
classNamestringMerged onto the root <ol> via cn().

Accessibility

  • Renders as a semantic <ol> so assistive tech announces it as an ordered list with item count.
  • The active step carries aria-current="step".
  • The component does not auto-label itself. Provide aria-label (e.g., "Recipe progress") or aria-labelledby on the root.
  • All decorative markup (markers, check icon, connector lines) is aria-hidden="true" — screen readers read titles and descriptions, not step numbers.
  • prefers-reduced-motion: reduce disables the staggered entry and the active pulse.

Credits

  • Extracted from: terminal-dreams (src/components/cookbook/StepTimeline.tsx). Generalized: stripped the project-specific CookbookStep data model and the click-to-navigate button behaviour; added orientation + connector props; replaced the "current vs completed" toggle with the canonical three-state (done/active/upcoming) cursor; added stagger + pulse motion tied to SPRINGS.smooth.