Journey Timeline
A vertical stepper that shows where someone is in a multi-stage process — completed stages, the current stage, and what's still ahead. Reusable for application journeys, onboarding checklists, order tracking, or any linear, ordered flow.
Preview
Anatomy
Each row shares the same four parts. The row itself, not the connecting line between rows, carries the meaning — the line is decorative.
| Part | Purpose |
|---|---|
| Icon circle | Filled with the accent color for completed/active stages; a neutral tint for upcoming ones. Icon communicates the stage's meaning, not just its position. |
| Stage label | The stage name. Bold for the active stage; secondary weight for completed; lowest-emphasis tertiary color for upcoming — this weight ramp is what lets someone scan the whole list and immediately spot 'where am I' without reading every line. |
| Hint text | One line, shown only on the active stage by default ('Current stage') or an upcoming stage with an available action ('Tap to update'). Omit for completed stages — they don't need a call to action. |
| Trailing status | A checkmark for completed stages, a small filled dot for the active stage, or nothing for upcoming. Never the only signal of state — label weight and icon fill must also change. |
When to use
Linear, ordered stages
The process has a fixed, known sequence — applied → screen → interview → offer, or order placed → shipped → delivered. Each stage happens after the last.
Small, bounded stage count
Roughly 3–7 stages. Below that, a simple progress bar communicates the same thing with less visual weight; above it, consider grouping into phases.
Non-linear or parallel states
If items can be in multiple states at once, or move backward and sideways (not just forward), use a Kanban board instead — a timeline implies a single forward path.
Continuous or percentage-based progress
A file upload or a habit-completion streak doesn't have discrete named stages — use a progress bar or the Goal & Streak pattern instead.
Implementation
type StageState = "completed" | "active" | "upcoming";
interface TimelineRowProps {
icon: React.ReactNode;
label: string;
state: StageState;
hint?: string;
trailing?: React.ReactNode;
}
// A single stage in a vertical stepper timeline: status icon, stage label,
// current-state hint text, and an optional trailing status glyph.
export function TimelineRow({ icon, label, state, hint, trailing }: TimelineRowProps) {
const isFilled = state === "completed" || state === "active";
return (
<div
className="flex items-center gap-4 py-2.5"
role="listitem"
aria-current={state === "active" ? "step" : undefined}
>
<div
className={`flex h-[34px] w-[34px] shrink-0 items-center justify-center rounded-full ${
isFilled ? "bg-[rgb(var(--accent))] text-white" : "bg-[rgb(var(--text-primary)/0.08)] text-[rgb(var(--text-secondary))]"
}`}
>
{icon}
</div>
<div className="flex flex-col gap-0.5">
<span
className={`text-[15px] ${
state === "active"
? "font-bold text-[rgb(var(--text-primary))]"
: state === "completed"
? "font-normal text-[rgb(var(--text-secondary))]"
: "font-normal text-[rgb(var(--text-tertiary))]"
}`}
>
{label}
</span>
{hint && (
<span
className={`text-[12px] ${
state === "active" ? "text-[rgb(var(--accent))]" : "text-[rgb(var(--text-tertiary))]"
}`}
>
{hint}
</span>
)}
</div>
<div className="ml-auto">{trailing}</div>
</div>
);
}Accessibility
- →Group each row's icon, label, and hint into a single accessibility element (aria via a wrapping container role, or .accessibilityElement(children: .combine) in SwiftUI) so VoiceOver/TalkBack announce one coherent stop per stage, not four fragments.
- →Mark the active stage with aria-current="step" (web) or an equivalent selected trait (SwiftUI) — don't rely on color or font weight alone to convey which stage is current.
- →State (completed/active/upcoming) must be distinguishable without color: pair it with the trailing glyph (✓, dot) and label copy ('Current stage'), since color-only differences fail for colorblind users and in monochrome contexts.
- →If a row is tappable (e.g. to log an update), give it an accessible label that states the stage name and its state together: 'Interview, current stage' — not just 'Interview'.
- →Wrap the whole sequence in role="list" and each row in role="listitem" on the web so assistive tech announces the row count and position ('item 3 of 4').