Skip to main content
Urbicon UI
source

JourneyTimelinebeta

A retrospective timeline that holds one event in focus with rich detail and keeps the rest as quiet context along a time axis.

Playground

  1. 3 Jun
    2 days · validation
  2. 5 Jun
    manual review
  3. 6 Jun

    Full record for “Statements” renders here while the node is in focus.

Orientation
Detail
Size
<script lang="ts">
  import { JourneyTimeline } from '@urbicon-ui/blocks';

  const items = [
    { id: 'readings', title: 'Meter readings', status: 'complete', subtitle: 'All units collected', meta: '3 Jun', segmentLabel: '2 days · validation' },
    { id: 'validate', title: 'Validation', status: 'complete', subtitle: 'Anomalies resolved', meta: '5 Jun', connector: 'dashed', segmentLabel: 'manual review' },
    { id: 'statements', title: 'Statements', status: 'active', subtitle: 'Generating documents', meta: '6 Jun' },
    { id: 'dispatch', title: 'Dispatch', status: 'pending', subtitle: 'Email + postal' }
  ];
</script>

<JourneyTimeline
  {items}
>
  {#snippet node(item)}
    <p class="text-text-secondary text-sm">
      Full record for “{item.title}” renders here while the node is in focus.
    </p>
  {/snippet}
</JourneyTimeline>

01 Examples

Each node needs an id, a title and a status. subtitle, meta (its label on the time axis), connector and segmentLabel are optional. One focusable node holds focus at a time and renders its detail through the node snippet, starting at defaultFocusId or the first active node and readable or drivable with bind:focusId.

Vertical chronicle with inline detail

The default. Per-node meta renders on the chronicle rail (here through the meta snippet: planned date + confirmed Badge), connectors carry meaning (dashed = manual hop), segment labels annotate the stretch between nodes. The focused node becomes an elevated card in place.
Focused node: statements
  1. 3 Jun 3 Jun
    2 days · validation
  2. 5 Jun 6 Jun
    manual review
  3. 6 Jun

    Full record for Statements: assignments, anomalies and the audit trail live here.

    dispatch queue
<div class="flex w-full max-w-lg flex-col gap-3">
  <div class="flex items-center gap-2">
    <span class="text-text-tertiary text-xs font-medium">Focused node:</span>
    <Badge size="xs" intent="primary" variant="soft">{focusId}</Badge>
  </div>
  <JourneyTimeline items={billing} bind:focusId>
    {#snippet meta(item)}
      <div class="flex flex-col items-end gap-1">
        {#if item.meta}
          <span class="text-text-tertiary font-mono text-xs tabular-nums">{item.meta}</span>
        {/if}
        {#if actuals[item.id]}
          <Badge size="xs" intent="success" variant="soft">{actuals[item.id]}</Badge>
        {/if}
      </div>
    {/snippet}
    {#snippet node(item)}
      <div class="flex flex-col gap-2 py-0.5">
        <p class="text-text-secondary text-sm">
          Full record for <strong>{item.title}</strong>: assignments, anomalies and the audit
          trail live here.
        </p>
        <div>
          <Button size="sm" variant="outlined">Open {item.title.toLowerCase()}</Button>
        </div>
      </div>
    {/snippet}
  </JourneyTimeline>
</div>

Stable readout with detail=panel

The rail stays rigid: rows never change height. The focused node's detail renders in a panel beside the rail on wide viewports and docks to the viewport bottom on narrow ones. Reach for this on long chronicles, or when the detail is tall.
  1. Mon
  2. Tue
  3. Wed
    customs clearance

In transit

Scans, carrier and location history for this stage.

customs clearance
<div class="w-full">
  <JourneyTimeline items={shipment} detail="panel">
    {#snippet node(item)}
      <div class="flex flex-col items-start gap-2">
        <p class="text-text-primary text-sm font-medium">{item.title}</p>
        <p class="text-text-secondary text-sm">
          Scans, carrier and location history for this stage.
        </p>
        {#if item.segmentLabel}
          <Badge size="xs" intent="neutral" variant="soft">{item.segmentLabel}</Badge>
        {/if}
      </div>
    {/snippet}
  </JourneyTimeline>
</div>

Horizontal lifecycle

Horizontal always renders the shared panel below the rail: the same rail and readout model, rotated. Meta renders as a kicker line above each title.
  1. 1 Mar
  2. 5 Mar
  3. 19 Mar

Invoice events during “Open”: issued, reminders, payments.

<div class="w-full">
  <JourneyTimeline items={lifecycle} orientation="horizontal">
    {#snippet node(item)}
      <p class="text-text-secondary text-sm">
        Invoice events during “{item.title}”: issued, reminders, payments.
      </p>
    {/snippet}
  </JourneyTimeline>
</div>

Cockpit rows: glyph markers, attention and trailing

The rich-row recipe: the marker snippet puts glyphs inside the status dots (scaled up via slotClasses.marker), status attention flags the optional-but-noteworthy row, and the trailing snippet adds badges and a help action per row. Trailing renders outside the trigger button, so pressing the help button never moves the focus.

No help requested yet.

  1. must · blocks close
  2. Consumption Waits for heating Pending
  3. Why “Expenses & distribution” matters, its consequences and the next action live here.

  4. optional
<div class="flex w-full max-w-lg flex-col gap-3">
  <p class="text-text-tertiary text-xs" aria-live="polite">
    {lastHelp ? `Help requested for “${lastHelp}”.` : 'No help requested yet.'}
  </p>
  <JourneyTimeline
    items={cockpit}
    slotClasses={{ marker: 'size-5 mt-1.5', markerColumn: 'w-5' }}
  >
    {#snippet marker(item)}
      <span class={['text-2xs leading-none font-bold', glyphTone[item.status]]}>
        {glyphs[item.status]}
      </span>
    {/snippet}
    {#snippet trailing(item)}
      {#if item.status === 'blocked'}
        <Badge size="xs" intent="danger" variant="soft">must · blocks close</Badge>
      {:else if item.status === 'attention'}
        <Badge size="xs" intent="warning" variant="soft">optional</Badge>
      {/if}
      {#if item.focusable !== false}
        <Button size="xs" variant="ghost" onclick={() => (lastHelp = item.title)}>?</Button>
      {/if}
    {/snippet}
    {#snippet node(item)}
      <p class="text-text-secondary text-sm">
        Why “{item.title}” matters, its consequences and the next action live here.
      </p>
    {/snippet}
  </JourneyTimeline>
</div>

02 Statuses

Each node's status sets its marker and its title tone:

statusMarkerMeaning
completeFilled success dotDone. The connector leaving it reads as “travelled”.
activeRinged primary dotThe step in progress right now.
pendingHollow dotWork not yet started.
attentionHollow warning dotWorth a look, but it does not block.
blockedDanger dot, and the title turns danger tooA hard stop, so colour is never the only cue.
skippedMuted dotNot applicable to this run.

Set focusable: false for pure waypoints: they render a marker and label but never take focus and are skipped by keyboard navigation.

All statuses

A node per status, including a non-focusable automatic step.
  1. This node is active. Its detail renders only while focused.

  2. Automatic hold System step Pending
<div class="w-full max-w-lg">
  <JourneyTimeline items={statuses} defaultFocusId="s-active">
    {#snippet node(item)}
      <p class="text-text-secondary text-sm">
        This node is <code>{item.status}</code>. Its detail renders only while focused.
      </p>
    {/snippet}
  </JourneyTimeline>
</div>

03 JourneyTimeline vs. Stepper vs. Tab

Three components, three different jobs, and the overlap is smaller than it looks:

ComponentIts jobReach for it when
JourneyTimelineRetrospective observationAn ordered record of what happened or where things stand: shipment tracking, audit trails, billing runs, travel logs. Time (meta) is the first axis, and connectors and segment labels describe the stretches between events. Focus reveals detail. It never advances a process.
StepperProspective processA wizard the user walks through: checkout, onboarding, multi-step forms. Steps are tasks to complete (often with embedded inputs), progress moves forward, and the component may gate navigation. No time axis. The user is the timeline.
TabPeer viewsUnordered, equivalent surfaces of one thing: Account, Billing, Team. No sequence, no status, no chronology. If reordering the items would change their meaning, they are not tabs.

Rule of thumb: does each item carry a status and does their order tell a story? JourneyTimeline. Does the user complete the items one by one? Stepper. Are the items just alternative views? Tab.

04 Customization

A primary-accented readout

slotClasses haloes the markers and tints the docked panel into a milestone look the props don't offer. It stays token-native, so it tracks light and dark, and keeps the panel's radius tier, shadow and docking behaviour.

Comments, reviewers and version history for “In review” live here.

<div class="w-full">
  <JourneyTimeline
    items={approval}
    detail="panel"
    slotClasses={{
      marker: 'ring-2 ring-primary/30 ring-offset-2 ring-offset-surface-base',
      panel: 'bg-surface-selected border-primary/40'
    }}
  >
    {#snippet node(item)}
      <p class="text-text-secondary text-sm">
        Comments, reviewers and version history for “{item.title}” live here.
      </p>
    {/snippet}
  </JourneyTimeline>
</div>

This is one of five ways to restyle a block. See Customization for class, slotClasses, unstyled, preset and provider-level overrides.

05 Accessibility

Structure and ARIA

The rail is an ordered list. Each node carries aria-current="step" while its status is active. The focusable trigger exposes aria-expanded and aria-controls for its detail region (a per-node inline region, or the shared panel in panel/horizontal mode). The status is announced through a visually-hidden label, so the dot markers stay decorative, including any glyphs rendered through the marker snippet.

Trailing content stays outside the trigger

trailing content renders outside the trigger button, as a sibling in the header row: buttons and links inside it are valid HTML, become regular tab stops after the header, and activating them never changes the focused node. Arrow-key roving stays on the node headers only.

Keyboard

  • / (vertical) or / (horizontal) move the roving focus between node headers without changing the focused node.
  • Home / End jump to the first / last focusable node.
  • Enter / Space put the header's node in focus.

Motion

Expand/collapse runs on the motion-duration tokens, which collapse to 1 ms under prefers-reduced-motion: reduce. When activating a node makes another card collapse above it, the component counter-scrolls so the activated header stays visually stationary. Real user scrolling cancels it immediately.

06 API Reference

17 props
17 props 1 required
Prop
Type
Default
Description

07 Types

Local type definitions used by this component.

7 types
Name
Kind
Category
Used by
Description

08 Installation

Import

import { JourneyTimeline, type JourneyNode } from '@urbicon-ui/blocks';