A record of things that happened, in order: activity feeds, deploy logs, order tracking, audit trails, company history.
Timeline or stepper?
PetalComponents.Stepper is interactive progress - clickable steps in a
flow the user is moving through, with a notion of "where you are" that the app
changes as they work. A timeline is a record - an append-only display of
events. Nothing in it is clickable, nothing pushes an event, and the entries
are whatever your data says they are. If the user is meant to navigate it,
reach for stepper/1; if they are meant to read it, reach for timeline/1.
Anatomy
The root is an <ol> and each entry is an <li>, so screen readers announce
position and count with no ARIA at all. Every entry has a marker on a rail, a
connector to the next entry, and content: an optional time, title and
description, plus an inner block for anything richer.
<.timeline>
<:item time="9:41am" title="Order placed" description="Payment authorised." />
<:item time="11:02am" title="Packed" description="Left the Melbourne warehouse." />
<:item time="Tomorrow" title="Delivered" state="upcoming" />
</.timeline>Variants
default is a left rail with the content beside it. alternating swings
entries either side of a centre rail on md and up (collapsing back to
default below). compact is activity-feed density - tighter spacing and
smaller type, which is what you want with avatar markers:
<.timeline variant="compact">
<:item
marker="avatar"
src={~p"/images/alex.jpg"}
name="Alex Chen"
time="12 minutes ago"
title="Alex pushed 3 commits"
/>
<:item marker="icon" icon="hero-check-circle" color="success" time="10 minutes ago" title="CI passed" />
</.timeline>orientation="horizontal" is the milestones layout instead - markers on a
horizontal rail with content underneath, scrolling sideways with CSS
scroll-snap when the row runs out of room. The variant attr is vertical-only
and is ignored when horizontal.
Leading times
time_placement="start" moves the time out of the entry and into a column of
its own on the far side of the rail, right-aligned against it, with the title
and body to the right. It is the layout for a log you scan by when rather than
by what - deploys, audit trails, a day's worth of events:
<.timeline time_placement="start">
<:item time="12 minutes ago" title="Deployed to production" />
<:item time="1 hour ago" title="Merged #641" />
</.timeline>The column is --pc-timeline-time-col wide (8rem, which clears "15 minutes
ago" with room over). Set the property on the timeline to retune it:
<.timeline time_placement="start" style="--pc-timeline-time-col: 11rem">Below sm there is no room for a third column, so the time falls back to the
default placement above the title and the markers stay exactly where they
were. Vertical default and compact only: alternating and
orientation="horizontal" already spend the axis this column needs, so they
accept the attr and ignore it, the same way variant is ignored when
horizontal.
Markers
marker takes "dot" (the default), "icon" (pass icon), "avatar" (pass
src and/or name, exactly as PetalComponents.Avatar.avatar/1 takes them),
or "number", which prints the entry's 1-based position for you. color
paints the marker in any of the seven semantic colours.
States
state is "complete" (default), "current", "loading" or "upcoming".
The current entry is ringed and carries aria-current="step"; upcoming
entries render muted with a hollow marker, and the connector running down to
them is de-emphasised.
"loading" is the entry that is happening while you watch it - a deploy
mid-roll, a file still uploading. Its marker spins:
<.timeline variant="compact">
<:item marker="icon" icon="hero-check-circle" color="success" time="9 minutes ago" title="CI passed" />
<:item marker="icon" state="loading" time="just now" title="Deploying v4.14.0 to production" />
</.timeline>A loading marker shows the spinner instead of its icon, number or avatar,
whatever marker says - a 10px dot has nowhere to put one - so the entry
keeps the chip and the semantic colour but loses its glyph for the duration.
The entry carries aria-busy="true", and the spin stops under
prefers-reduced-motion.
Accessibility
There is no WAI-ARIA pattern for a timeline, so this leans on native list semantics rather than inventing a role.
<ol>root,<li>entries. Noroleoverrides, no interactive wrappers.aria-current="step"on the current entry, and nowhere else.aria-busy="true"on a loading entry, and nowhere else.- The rail (markers and connectors) is decorative and
aria-hidden. State is never conveyed by colour alone -current,loadingandupcomingentries each carry a visually-hidden label. - The horizontal scroll container takes focus so it can be scrolled by
keyboard, with a visible focus ring, and its smooth scrolling is behind a
prefers-reduced-motionguard.
Rich content in the inner block renders below the description, in every variant:
<.timeline>
<:item time="2 hours ago" title="Deployed v4.2.0 to production">
<.card class="mt-3">
<.card_content>Rolled out to all regions in 3m 12s.</.card_content>
</.card>
</:item>
</.timeline>
Summary
Functions
Renders a timeline of events.
Functions
Renders a timeline of events.
See PetalComponents.Timeline for the variants, markers, states and the
accessibility contract.
Attributes
orientation(:string) - horizontal is the milestones layout; scroll-snaps on small screens. Defaults to"vertical". Must be one of"vertical", or"horizontal".variant(:string) - 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).Defaults to
"default". Must be one of"default","alternating", or"compact".connector(:string) - line style of the rail between markers. Defaults to"solid". Must be one of"solid", or"dashed".time_placement(:string) - 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).Defaults to
"top". Must be one of"top", or"start".label(:string) - 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. Defaults tonil.class(:any) - CSS class on the root list. Defaults tonil.Global attributes are accepted.
Slots
item(required) - one entry per event, rendered in author order. Accepts attributes:title(:string) - entry heading.time(:string) - plain-text timestamp; omit and slot in <.local_time> yourself for client-side formatting.description(:string) - one-line body; use the inner block instead for rich content.marker(:string) - "dot" (default), "icon", "avatar", or "number".icon(:string) - heroicon name when marker="icon", e.g. "hero-truck". Defaults to "hero-check" when omitted.src(:string) - image URL when marker="avatar".name(:string) - fallback initials source when marker="avatar" and src is absent (matches <.avatar> behaviour).color(:string) - semantic colour of the marker: "primary" (default), "secondary", "gray", "info", "success", "warning", "danger".state(:string) - "complete" (default), "current", "loading", or "upcoming". current gets a ringed marker + aria-current="step"; loading spins the marker and sets aria-busy; upcoming renders muted with the connector into it de-emphasised.class(:any) - CSS class on this entry's <li>.