Timeline
A vertical list of dated events on a thin rail.
Activity
- Aisha Bello changed status from In progress to Resolved
- Aisha Bello left a noteHalcyon short-paid INV-20944 by $412.50 for a damaged pallet. Credit memo CM-3381 is issued and matched.
- The first-response SLA breached
- Priya Raman assigned it to Aisha Bello
- Payment status agent created the ticket from a call with Halcyon
import { Timeline, TimelineBody, TimelineHeader, TimelineItem, TimelineTime,} from "@oration/canon/components/timeline";import { AlarmClockIcon, CircleCheckIcon, MessageSquareTextIcon, PlusIcon, UserRoundIcon,} from "lucide-react";export function Hero() { return ( <div className="w-full max-w-xl rounded-xl bg-card p-4 text-left shadow-border"> <h3 className="mb-4 text-sm font-semibold">Activity</h3> <Timeline> <TimelineItem icon={<CircleCheckIcon />} tone="success"> <TimelineHeader> <span> <strong>Aisha Bello</strong> changed status from{" "} <strong>In progress</strong> to{" "} <strong>Resolved</strong> </span> <TimelineTime dateTime="2026-09-28T11:42"> 11:42 AM </TimelineTime> </TimelineHeader> </TimelineItem> <TimelineItem icon={<MessageSquareTextIcon />}> <TimelineHeader> <span> <strong>Aisha Bello</strong> left a note </span> <TimelineTime dateTime="2026-09-28T10:05"> 10:05 AM </TimelineTime> </TimelineHeader> <TimelineBody className="rounded-[10px] bg-muted/70 px-3 py-2"> Halcyon short-paid INV-20944 by $412.50 for a damaged pallet. Credit memo CM-3381 is issued and matched. </TimelineBody> </TimelineItem> <TimelineItem icon={<AlarmClockIcon />} tone="danger"> <TimelineHeader> <span>The first-response SLA breached</span> <TimelineTime dateTime="2026-09-28T09:44"> 9:44 AM </TimelineTime> </TimelineHeader> </TimelineItem> <TimelineItem icon={<UserRoundIcon />}> <TimelineHeader> <span> <strong>Priya Raman</strong> assigned it to{" "} <strong>Aisha Bello</strong> </span> <TimelineTime dateTime="2026-09-28T09:18"> 9:18 AM </TimelineTime> </TimelineHeader> </TimelineItem> <TimelineItem icon={<PlusIcon />}> <TimelineHeader> <span> <strong>Payment status agent</strong> created the ticket from a call with Halcyon </span> <TimelineTime dateTime="2026-09-28T09:14"> 9:14 AM </TimelineTime> </TimelineHeader> </TimelineItem> </Timeline> </div> );}Usage#
Timeline is a vertical list of dated events on a thin rail: who changed a ticket's status, when an SLA breached, when a payment run was approved and sent. Each event is one sentence with the actor and the object in medium weight, a 28px icon disc on the rail and the time at the right. It's the activity log on ticket and record pages and the overnight feed on Home. The common mistake is writing events as dot-joined fragments, or coloring every icon; tones are for verdicts such as a breach or a resolution.
When to use
- For a record's history: status changes, assignments, labels, SLA events on a ticket.
- For the steps of a run in order: scheduled, approved, remittances sent, payments failed.
- For a feed of what happened overnight across accounts, with the company linked in each event.
- When an event carries a note or a reply that people read in place, through
TimelineBody.
When not to use
- For a plan or checklist whose steps tick from queued to done. Use Task list
- For progress through named stages of a pipeline. Use Stepper
- For a conversation between people. Use message rows. Use Message
- For a call's transcript with speakers and timestamps. Use Live transcript
- For a sortable log with many columns, such as webhook deliveries. Use Data grid
The Layout Over Punctuation Rule
The Quiet Indigo Rule
primary tone is for live state with a label, such as an agent on a call right now. It never decorates agent or enrichment events.The Label-Beside-Color Rule
Anatomy#
- Wen Zhou left a noteW-9 received from Orchard Street.
- Jordan Lee created the ticket
- Icon disc. A 28px circle on the White Plane with the hairline lift, holding a 14px icon in Slate Meta or the item's tone.
- Rail. A 1px Hairline from under one disc to just above the next. The last item has none.
- Header.
TimelineHeader: 13px Slate Meta that wraps, with<strong>actors and objects in weight 500 Graphite Ink. - Time.
TimelineTime: a<time>in 12px Faint Slate and tabular figures, pushed to the right edge. - Body. Optional
TimelineBody, 8px under the header: a reply, a note in a tint well, or an action.
Examples#
Tones
Neutral for most events. Success and danger for verdicts, warning for something at risk, and primary only for something live, beside a label that says so.
- Payment status agent is on a call with HalcyonLive
- Aisha Bello resolved the ticket
- First response is due in 15 minutes
- The first-response SLA breached
- Jordan Lee added the label Short pay
import { StatusLabel } from "@oration/canon/components/status-dot";import { Timeline, TimelineHeader, TimelineItem, TimelineTime } from "@oration/canon/components/timeline";import { AlarmClockIcon, CircleCheckIcon, PhoneIcon, TagIcon } from "lucide-react";export function Tones() { return ( <div className="w-full max-w-xl text-left"> <Timeline> <TimelineItem icon={<PhoneIcon />} tone="primary"> <TimelineHeader> <span> <strong>Payment status agent</strong> is on a call with Halcyon </span> <StatusLabel tone="primary" pulse className="ml-auto text-xs" > Live </StatusLabel> </TimelineHeader> </TimelineItem> <TimelineItem icon={<CircleCheckIcon />} tone="success"> <TimelineHeader> <span> <strong>Aisha Bello</strong> resolved the ticket </span> <TimelineTime dateTime="2026-09-28T11:42"> 11:42 AM </TimelineTime> </TimelineHeader> </TimelineItem> <TimelineItem icon={<AlarmClockIcon />} tone="warning"> <TimelineHeader> <span>First response is due in 15 minutes</span> <TimelineTime dateTime="2026-09-28T09:29"> 9:29 AM </TimelineTime> </TimelineHeader> </TimelineItem> <TimelineItem icon={<AlarmClockIcon />} tone="danger"> <TimelineHeader> <span>The first-response SLA breached</span> <TimelineTime dateTime="2026-09-28T09:44"> 9:44 AM </TimelineTime> </TimelineHeader> </TimelineItem> <TimelineItem icon={<TagIcon />}> <TimelineHeader> <span> <strong>Jordan Lee</strong> added the label{" "} <strong>Short pay</strong> </span> <TimelineTime dateTime="2026-09-28T09:16"> 9:16 AM </TimelineTime> </TimelineHeader> </TimelineItem> </Timeline> </div> );}With a body
TimelineBody holds what people read in place: a reply in Slate Meta, or an internal note in a tint well.
- Tomás Ferreira replied to Northwind FreightThe remittance for INV-20931 went out by ACH on Sep 25. Your bank should post it by Tuesday; the trace number is below.
- Priya Raman left an internal noteNorthwind changed banks in August. Check the new account is verified before we resend anything.
- Payment status agent transferred the callThe supplier asked for an ACH trace number the agent couldn't look up.
import { Timeline, TimelineBody, TimelineHeader, TimelineItem, TimelineTime,} from "@oration/canon/components/timeline";import { BotIcon, MessageSquareTextIcon, SendIcon } from "lucide-react";export function WithBody() { return ( <div className="w-full max-w-xl text-left"> <Timeline> <TimelineItem icon={<SendIcon />}> <TimelineHeader> <span> <strong>Tomás Ferreira</strong> replied to Northwind Freight </span> <TimelineTime dateTime="2026-09-28T14:02"> 2:02 PM </TimelineTime> </TimelineHeader> <TimelineBody className="text-muted-foreground"> The remittance for INV-20931 went out by ACH on Sep 25. Your bank should post it by Tuesday; the trace number is below. </TimelineBody> </TimelineItem> <TimelineItem icon={<MessageSquareTextIcon />}> <TimelineHeader> <span> <strong>Priya Raman</strong> left an internal note </span> <TimelineTime dateTime="2026-09-28T13:40"> 1:40 PM </TimelineTime> </TimelineHeader> <TimelineBody className="rounded-[10px] bg-muted/70 px-3 py-2"> Northwind changed banks in August. Check the new account is verified before we resend anything. </TimelineBody> </TimelineItem> <TimelineItem icon={<BotIcon />}> <TimelineHeader> <span> <strong>Payment status agent</strong> transferred the call </span> <TimelineTime dateTime="2026-09-28T13:31"> 1:31 PM </TimelineTime> </TimelineHeader> <TimelineBody className="text-muted-foreground"> The supplier asked for an ACH trace number the agent couldn't look up. </TimelineBody> </TimelineItem> </Timeline> </div> );}Run history
The steps of a payment run, newest first, with the fix for the failed step right in its body.
Payment run, Friday, Sep 25
$1,184,902.40- 3 payments failedNorthwind Freight, Halcyon and Orchard Street. Their banks returned the ACH files.
- 209 remittances sent to suppliers
- Maya Okafor approved the run
- Wen Zhou scheduled 212 invoices for Sep 25
import { Button } from "@oration/canon/components/button";import { Timeline, TimelineBody, TimelineHeader, TimelineItem, TimelineTime,} from "@oration/canon/components/timeline";import { toast } from "@oration/canon/components/toast";import { CalendarClockIcon, CheckIcon, CircleXIcon, FlagIcon, RotateCcwIcon,} from "lucide-react";export function RunHistory() { return ( <section aria-labelledby="run-history" className="w-full max-w-xl rounded-xl bg-card p-4 text-left shadow-border" > <div className="mb-4 flex items-baseline justify-between"> <h3 id="run-history" className="text-sm font-semibold"> Payment run, Friday, Sep 25 </h3> <span className="text-xs text-muted-foreground tabular-nums"> $1,184,902.40 </span> </div> <Timeline> <TimelineItem icon={<CircleXIcon />} tone="danger"> <TimelineHeader> <span> <strong>3 payments failed</strong> </span> <TimelineTime dateTime="2026-09-25T14:26"> Sep 25, 2:26 PM </TimelineTime> </TimelineHeader> <TimelineBody className="flex flex-col items-start gap-2"> <span className="text-muted-foreground"> Northwind Freight, Halcyon and Orchard Street. Their banks returned the ACH files. </span> <Button type="button" variant="outline" size="xs" onClick={() => toast.add({ type: "info", title: "Retrying 3 payments", description: "They go out with Friday's run on Oct 2.", }) } > <RotateCcwIcon data-icon="inline-start" aria-hidden="true" /> Retry failed payments </Button> </TimelineBody> </TimelineItem> <TimelineItem icon={<CheckIcon />} tone="success"> <TimelineHeader> <span> <strong>209 remittances</strong> sent to suppliers </span> <TimelineTime dateTime="2026-09-25T14:20"> Sep 25, 2:20 PM </TimelineTime> </TimelineHeader> </TimelineItem> <TimelineItem icon={<FlagIcon />}> <TimelineHeader> <span> <strong>Maya Okafor</strong> approved the run </span> <TimelineTime dateTime="2026-09-24T16:58"> Sep 24, 4:58 PM </TimelineTime> </TimelineHeader> </TimelineItem> <TimelineItem icon={<CalendarClockIcon />}> <TimelineHeader> <span> <strong>Wen Zhou</strong> scheduled 212 invoices for Sep 25 </span> <TimelineTime dateTime="2026-09-23T10:12"> Sep 23, 10:12 AM </TimelineTime> </TimelineHeader> </TimelineItem> </Timeline> </section> );}No activity yet
A new ticket's log is empty until something happens. Say what will show up here.
No activity yet
import { EmptyState } from "@oration/canon/components/data-state";export function NoActivity() { return ( <div className="w-full max-w-sm rounded-xl bg-card shadow-border"> <EmptyState size="sm" illustration="schedule" title="No activity yet" description="Status changes, assignments and SLA events show up here." /> </div> );}States#
| State | Treatment |
|---|---|
| Neutral | The default: icon in Slate Meta. Most events. |
| Primary | Icon in Quiet Indigo. Only for something live, beside a label that says so. |
| Success | Icon in Ledger Green: resolved, passed, sent. |
| Warning | Icon in Amber Ink (Caution Amber in dark): at risk, due soon. |
| Danger | Icon in Signal Red: breached, failed. |
| Last item | No rail below it and no bottom padding. |
| Loading | SkeletonList with icon, which matches the disc and one line per event. |
| Empty | An EmptyState with the schedule illustration: No activity yet and what will show up here. |
Behavior#
- Timeline is an ordered list (
<ol>) of<li>items. Order it newest first on record pages and oldest first for the steps of a run. - Each item is a two-column grid: a 28px column for the disc and the content beside it, 12px apart, 20px between items.
TimelineHeaderwraps, andTimelineTimesits at the end of the last line withml-auto, so long sentences keep the time at the right.- Nothing in it is interactive by itself. Put links on the object (a company, a ticket) and buttons in the body.
- No entrance animation. New events can appear at the top without moving focus.
Do and don't#
- Priya Raman assigned it to Aisha Bello
<strong>.- Priya Raman · Assigned · Aisha Bello · 9:18 AM
- The first-response SLA breached
- Priya Raman assigned it
- Jordan Lee added a label
- The first-response SLA breached
- Priya Raman assigned it
- Jordan Lee added a label
Content#
- Past tense, actor first: Aisha Bello changed status from In progress to Resolved.
- System events name the system or say what happened: The first-response SLA breached, Payment status agent created the ticket from a call with Halcyon.
- Bold the actor and the values that changed, not the verb.
- Times: 9:18 AM for today, Sep 25, 2:26 PM for earlier, with a full ISO
dateTimeon the<time>. - Bodies quote the note or reply as written, or give one sentence of context and the action to take.
- Machine strings such as conversation IDs and invoice numbers can go in Geist Mono; names and figures stay in Geist Sans.
Accessibility#
- The list is an
<ol>, so screen readers announce the number of events and each one's position. - The icon disc is
aria-hidden; the header sentence must say what happened, since the icon and tone aren't announced. - Give
TimelineTimeadateTimeso the time is machine-readable, especially for relative labels such as Yesterday. - Keep the order the reader expects and say it in the section title when it isn't obvious, such as Newest first.
- Buttons and links in bodies need their own names: Retry failed payments, not Retry.
Design tokens#
| Token | Used for |
|---|---|
--background | Icon disc fill |
shadow-border | Icon disc edge |
--border | The rail |
--muted-foreground | Header text and neutral icons |
--foreground | Bold actors and values, body text |
--subtle-foreground | Times |
--primary | Primary icon tone |
--success | Success icon tone |
--warning-foreground | Warning icon tone, light |
--destructive | Danger icon tone |
API reference#
Timeline
The list. data-slot="timeline".
Other props spread onto <ol>.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | No default | Merged after flex flex-col. |
TimelineItem
One event: the disc, the rail and your content. data-slot="timeline-item".
Other props spread onto <li>.
| Prop | Type | Default | Description |
|---|---|---|---|
iconRequired | ReactNode | No default | A lucide icon; it's sized to 14px for you. |
tone | "neutral" | "primary" | "success" | "warning" | "danger" | "neutral" | Colors the icon. Neutral unless the event is a verdict or live. |
className | string | No default | Merged onto the <li>. |
TimelineHeader
The event sentence and its time, on a wrapping line.
Other props spread onto <div>.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | No default | Merged after the defaults. |
TimelineTime
The event's time, at the right of the header.
Other props spread onto <time>.
| Prop | Type | Default | Description |
|---|---|---|---|
dateTime | string | No default | The ISO date and time, for machines and screen readers. |
className | string | No default | Merged after the defaults. |
TimelineBody
Content under the header: a note, a reply or an action.
Other props spread onto <div>.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | No default | Add rounded-[10px] bg-muted/70 px-3 py-2 for a note in a tint well, or text-muted-foreground for a quoted reply. |
Known gaps#
Where the implementation and the system disagree today. Follow the system, not the gap.
TimelineHeader and TimelineBody ask for 13px, but they merge classes with cn from the cn package, which reads text-13 as a color and drops it beside text-muted-foreground and text-foreground. Both render at the inherited size, usually 14px.
Home's overnight feed gives agent and enrichment events tone="primary", which tints their icons indigo as decoration. The Quiet Indigo Rule keeps indigo for live state with a label.
TimelineTime doesn't require dateTime, and the overnight feed passes none, so its times aren't machine-readable.
icon is required on every item, even for minor events where a small dot would do.
There is no timeline skeleton; the ticket activity log borrows SkeletonList with icon, which is close but draws no rail.