Components Timeline

Petal Pro is the full SaaS app this is built for

Auth, billing, admin, and Claude Code integration included. One purchase, unlimited projects.

Timeline

Events down a rail - icons, avatars or dots as markers, solid or dashed connectors, and a loading state for streams that have not finished.
Order tracking

The default left rail. Icon markers carry the meaning of each step, the entry in flight is state="current" (ringed, and the only one with aria-current), and everything after it is upcoming - muted, hollow marker, faded line running into it.

  1. Mon 9:41am

    Order placed

    Payment authorised, receipt emailed.

  2. Mon 4:20pm

    Packed

    Picked and boxed at the Melbourne warehouse.

  3. Current
    Tue 7:05am

    Out for delivery

    With the courier, tracking updates every 20 minutes.

  4. Upcoming
    Tue, by 6pm

    Delivered

    Signature required.

heex
<.timeline label="Order history">
<:item
marker="icon"
icon="hero-check"
color="success"
time="Mon 9:41am"
title="Order placed"
description="Payment authorised, receipt emailed."
/>
<:item
marker="icon"
icon="hero-cube"
color="success"
time="Mon 4:20pm"
title="Packed"
description="Picked and boxed at the Melbourne warehouse."
/>
<:item
marker="icon"
icon="hero-truck"
state="current"
time="Tue 7:05am"
title="Out for delivery"
description="With the courier, tracking updates every 20 minutes."
/>
<:item
marker="icon"
icon="hero-home"
state="upcoming"
time="Tue, by 6pm"
title="Delivered"
description="Signature required."
/>
</.timeline>
Activity feed

variant="compact" is the density you want for a feed: tighter rhythm, smaller type, avatar markers. The last entry is state="loading" - the thing that is still happening while you read about it. Timestamps are plain strings here - swap in <.local_time format="relative"> and they stay right without a re-render.

  1. 12 minutes ago

    Alex pushed 3 commits to main

    Fix the retry backoff on the webhook worker

  2. 9 minutes ago

    CI passed

    418 tests, 2m 51s

  3. 6 minutes ago

    Sam approved the release

  4. In progress
    just now

    Deploying v4.14.0 to production

    Rolling through Sydney, Dublin and Ohio.

heex
<.timeline variant="compact" label="Recent activity">
<:item
marker="avatar"
name="Alex Chen"
time="12 minutes ago"
title="Alex pushed 3 commits to main"
description="Fix the retry backoff on the webhook worker"
/>
<:item
marker="icon"
icon="hero-check-circle"
color="success"
time="9 minutes ago"
title="CI passed"
description="418 tests, 2m 51s"
/>
<:item
marker="avatar"
name="Sam Rivera"
time="6 minutes ago"
title="Sam approved the release"
/>
<:item
marker="icon"
icon="hero-rocket-launch"
state="loading"
time="just now"
title="Deploying v4.14.0 to production"
description="Rolling through Sydney, Dublin and Ohio."
/>
</.timeline>
Pipeline steps

A CI run read top to bottom. Finished stages carry a check and how long they took, the stage in flight is state="loading" so its marker spins, and the one after it is upcoming. The title attr is a plain string, so anything that has to sit BESIDE the title - a duration chip, a status badge - goes in the entry's block on a flex row of its own. Reuse pc-timeline__title there and the row keeps the component's type and keeps fading when the entry is upcoming; mt-0 takes off the title's own first-line nudge, which a flex row does not want.

  1. Checkout code

    12s
    alex-chen

    Pushed a1b2c3d to main, 4 files changed.

  2. Install dependencies

    48s

    Restored the cache, 3 of 214 packages fetched.

  3. In progress

    Unit & integration tests

    Running

    311 of 418 tests, 2 shards on ubuntu-24.04.

  4. Upcoming

    Production build

    Pending
heex
<.timeline label="Pipeline run 482">
<:item marker="icon" icon="hero-check" color="success">
<div class="flex flex-wrap items-center gap-2">
<h3 class="mt-0 pc-timeline__title">Checkout code</h3>
<.badge size="sm" color="success" variant="light" label="12s" />
</div>
<div class="p-3 mt-2 border border-gray-200 rounded-lg dark:border-gray-400/17">
<div class="flex items-center gap-2">
<.avatar name="Alex Chen" alt="Alex Chen" size="2xs" />
<span class="text-xs font-medium text-gray-700 dark:text-gray-300">alex-chen</span>
</div>
<p class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
Pushed <span class="font-mono">a1b2c3d</span> to main, 4 files changed.
</p>
</div>
</:item>
<:item marker="icon" icon="hero-check" color="success">
<div class="flex flex-wrap items-center gap-2">
<h3 class="mt-0 pc-timeline__title">Install dependencies</h3>
<.badge size="sm" color="success" variant="light" label="48s" />
</div>
<p class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
Restored the cache, 3 of 214 packages fetched.
</p>
</:item>
<:item marker="icon" color="warning" state="loading">
<div class="flex flex-wrap items-center gap-2">
<h3 class="mt-0 pc-timeline__title">Unit & integration tests</h3>
<.badge size="sm" color="warning" variant="light" with_icon>
<span class="w-1.5 h-1.5 rounded-full bg-current" aria-hidden="true"></span> Running
</.badge>
</div>
<div class="p-3 mt-2 border border-gray-200 rounded-lg dark:border-gray-400/17">
<p class="text-xs text-gray-500 dark:text-gray-400">
311 of 418 tests, 2 shards on <span class="font-mono">ubuntu-24.04</span>.
</p>
</div>
</:item>
<:item marker="icon" icon="hero-cube" state="upcoming">
<div class="flex flex-wrap items-center gap-2">
<h3 class="mt-0 pc-timeline__title">Production build</h3>
<.badge size="sm" color="gray" variant="light" label="Pending" />
</div>
</:item>
</.timeline>
Deploy feed

time_placement="start" hands the time a column of its own beside the rail, right-aligned against it, which is the layout for a log you scan by when rather than by what. The column is 8rem of --pc-timeline-time-col and it only shows up from sm - narrower than that the time drops back above the title and the markers do not move.

  1. 12 minutes ago

    Deploy to production

    Success

    a1b2c3d · main · 42s

  2. 1 hour ago

    Deploy to production

    Failed

    9f4e210 · main · 18s

  3. Yesterday, 4:02pm

    Deploy to staging

    Success

    c81d0a7 · release/4.15 · 39s

heex
<.timeline time_placement="start" label="Recent deploys">
<:item color="success" time="12 minutes ago">
<div class="flex flex-wrap items-center gap-2">
<h3 class="mt-0 pc-timeline__title">Deploy to production</h3>
<.badge size="sm" color="success" variant="light" with_icon>
<span class="w-1.5 h-1.5 rounded-full bg-current" aria-hidden="true"></span> Success
</.badge>
</div>
<p class="mt-1 font-mono text-xs text-gray-500 dark:text-gray-400">
a1b2c3d · main · 42s
</p>
</:item>
<:item color="danger" time="1 hour ago">
<div class="flex flex-wrap items-center gap-2">
<h3 class="mt-0 pc-timeline__title">Deploy to production</h3>
<.badge size="sm" color="danger" variant="light" with_icon>
<span class="w-1.5 h-1.5 rounded-full bg-current" aria-hidden="true"></span> Failed
</.badge>
</div>
<p class="mt-1 font-mono text-xs text-gray-500 dark:text-gray-400">
9f4e210 · main · 18s
</p>
</:item>
<:item color="success" time="Yesterday, 4:02pm">
<div class="flex flex-wrap items-center gap-2">
<h3 class="mt-0 pc-timeline__title">Deploy to staging</h3>
<.badge size="sm" color="success" variant="light" with_icon>
<span class="w-1.5 h-1.5 rounded-full bg-current" aria-hidden="true"></span> Success
</.badge>
</div>
<p class="mt-1 font-mono text-xs text-gray-500 dark:text-gray-400">
c81d0a7 · release/4.15 · 39s
</p>
</:item>
</.timeline>
Rich entry bodies

Anything in an entry's inner block renders under the description, in every variant - a card, a diff, a thumbnail grid. Dashed connectors suit a log that has gaps in it.

  1. Yesterday

    Incident opened

    api-gateway returned 503 for 4% of requests over 11 minutes.
  2. Today

    Resolved

    Connection pool doubled.

    No customer impact Postmortem filed
heex
<.timeline connector="dashed">
<:item color="gray" time="Yesterday" title="Incident opened">
<div class="p-3 mt-3 text-sm border border-gray-200 rounded-lg text-gray-600 dark:border-gray-400/17 dark:text-gray-300">
<span class="font-mono text-xs">api-gateway</span>
returned 503 for 4% of requests over 11 minutes.
</div>
</:item>
<:item color="success" time="Today" title="Resolved" description="Connection pool doubled.">
<div class="flex gap-2 mt-3">
<.badge color="success" label="No customer impact" />
<.badge color="gray" label="Postmortem filed" />
</div>
</:item>
</.timeline>
Alternating

variant="alternating" swings entries either side of a centre rail from md up, and folds back to the default left rail below that - two columns of text on a phone is not a timeline, it's a puzzle.

  1. 2019

    Founded

    Two people, one office above a bakery.

  2. 2021

    Series A

    Team of 14, first 1,000 customers.

  3. 2023

    Went global

    Regions in Sydney, Dublin and Ohio.

heex
<.timeline variant="alternating">
<:item
marker="number"
time="2019"
title="Founded"
description="Two people, one office above a bakery."
/>
<:item
marker="number"
time="2021"
title="Series A"
description="Team of 14, first 1,000 customers."
/>
<:item
marker="number"
time="2023"
title="Went global"
description="Regions in Sydney, Dublin and Ohio."
/>
</.timeline>
Horizontal milestones

orientation="horizontal" puts the rail across the top with content underneath. It scrolls sideways with snap points when the row runs out of room, and the scroller takes keyboard focus so arrow keys work.

  1. Q1

    Research

    40 customer interviews.

  2. Q2

    Private beta

    200 teams on the waitlist.

  3. Current
    Q3

    Launch

    Public pricing goes live.

  4. Upcoming
    Q4

    Enterprise

    SSO, audit logs, SLAs.

heex
<.timeline orientation="horizontal" label="Company milestones">
<:item
marker="number"
color="gray"
time="Q1"
title="Research"
description="40 customer interviews."
/>
<:item
marker="number"
color="gray"
time="Q2"
title="Private beta"
description="200 teams on the waitlist."
/>
<:item
marker="number"
state="current"
time="Q3"
title="Launch"
description="Public pricing goes live."
/>
<:item
marker="number"
state="upcoming"
time="Q4"
title="Enterprise"
description="SSO, audit logs, SLAs."
/>
</.timeline>
Properties
Attribute Type Default Description
class any nil CSS class on the root list
connector string "solid" line style of the rail between markers
one of: "solid", "dashed"
label string nil accessible name for the list, announced instead of the generic "list" (e.g. "Order history"). Always set it on horizontal timelines: they are focusable scroll regions (tabindex=0), and a focusable region without a name is an a11y failure
orientation string "vertical" horizontal is the milestones layout; scroll-snaps on small screens
one of: "vertical", "horizontal"
rest global
time_placement string "top" where each entry's time sits. top: above the title, inside the entry (the default). start: in its own column on the far side of the rail, right-aligned against it, from sm up - below that it falls back to top. Vertical default and compact only; alternating and horizontal ignore it. Width is the --pc-timeline-time-col custom property (8rem).
one of: "top", "start"
variant string "default" vertical only, ignored when horizontal. default: left rail with entries to the right. alternating: entries alternate sides of a centre rail on md and up, collapsing to default below. compact: activity-feed density (tighter spacing, smaller type, suits avatar markers).
one of: "default", "alternating", "compact"
:item slot slot one entry per event, rendered in author order