PetalComponents.Timeline (petal_components v4.15.1)

Copy Markdown View Source

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. No role overrides, 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, loading and upcoming entries 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-motion guard.

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

timeline(assigns)

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 to nil.

  • class (:any) - CSS class on the root list. Defaults to nil.

  • 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>.