Skip to content

Timeline

A vertical list of dated events on a thin rail.

Status
Beta
Category
Data display
Adoption
Not used yet
import { Timeline } from "@oration/canon/components/timeline";
packages/canon/src/components/timeline.tsx

Activity

  1. Aisha Bello changed status from In progress to Resolved
  2. Aisha Bello left a note
    Halcyon short-paid INV-20944 by $412.50 for a damaged pallet. Credit memo CM-3381 is issued and matched.
  3. The first-response SLA breached
  4. Priya Raman assigned it to Aisha Bello
  5. 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

Events are sentences, never dot-joined fragments. Priya Raman assigned it to Aisha Bello with the time set apart by layout, not Priya Raman · Assigned · 9:18 AM.

The Quiet Indigo Rule

The 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

A danger or success icon is backed by the sentence: The first-response SLA breached. The color confirms it; the words carry it.

Anatomy#

  1. Wen Zhou left a note
    W-9 received from Orchard Street.
  2. Jordan Lee created the ticket
  1. 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.
  2. Rail. A 1px Hairline from under one disc to just above the next. The last item has none.
  3. Header. TimelineHeader: 13px Slate Meta that wraps, with <strong> actors and objects in weight 500 Graphite Ink.
  4. Time. TimelineTime: a <time> in 12px Faint Slate and tabular figures, pushed to the right edge.
  5. 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.

  1. Payment status agent is on a call with HalcyonLive
  2. Aisha Bello resolved the ticket
  3. First response is due in 15 minutes
  4. The first-response SLA breached
  5. 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.

  1. Tomás Ferreira replied to Northwind Freight
    The remittance for INV-20931 went out by ACH on Sep 25. Your bank should post it by Tuesday; the trace number is below.
  2. Priya Raman left an internal note
    Northwind changed banks in August. Check the new account is verified before we resend anything.
  3. Payment status agent transferred the call
    The 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
  1. 3 payments failed
    Northwind Freight, Halcyon and Orchard Street. Their banks returned the ACH files.
  2. 209 remittances sent to suppliers
  3. Maya Okafor approved the run
  4. 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

Status changes, assignments and SLA events show up here.
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#

States
StateTreatment
NeutralThe default: icon in Slate Meta. Most events.
PrimaryIcon in Quiet Indigo. Only for something live, beside a label that says so.
SuccessIcon in Ledger Green: resolved, passed, sent.
WarningIcon in Amber Ink (Caution Amber in dark): at risk, due soon.
DangerIcon in Signal Red: breached, failed.
Last itemNo rail below it and no bottom padding.
LoadingSkeletonList with icon, which matches the disc and one line per event.
EmptyAn 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.
  • TimelineHeader wraps, and TimelineTime sits at the end of the last line with ml-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#

  1. Priya Raman assigned it to Aisha Bello
Do. Write each event as a sentence, with the actor and object in <strong>.
  1. Priya Raman · Assigned · Aisha Bello · 9:18 AM
Don't. Join fragments with dots. Priya Raman · Assigned · Aisha Bello · 9:18 AM reads like a log dump.
  1. The first-response SLA breached
  2. Priya Raman assigned it
  3. Jordan Lee added a label
Do. Keep most icons neutral and color only verdicts: a breach in red, a resolution in green.
  1. The first-response SLA breached
  2. Priya Raman assigned it
  3. Jordan Lee added a label
Don't. Tint every icon indigo. The one event that matters no longer stands out, and indigo becomes decoration.

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 dateTime on 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 TimelineTime a dateTime so 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#

Design tokens
TokenUsed for
--backgroundIcon disc fill
shadow-borderIcon disc edge
--borderThe rail
--muted-foregroundHeader text and neutral icons
--foregroundBold actors and values, body text
--subtle-foregroundTimes
--primaryPrimary icon tone
--successSuccess icon tone
--warning-foregroundWarning icon tone, light
--destructiveDanger icon tone

API reference#

Timeline

The list. data-slot="timeline".

Other props spread onto <ol>.

Props of Timeline
PropTypeDefaultDescription
classNamestringNo defaultMerged after flex flex-col.

TimelineItem

One event: the disc, the rail and your content. data-slot="timeline-item".

Other props spread onto <li>.

Props of TimelineItem
PropTypeDefaultDescription
iconRequiredReactNodeNo defaultA 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.
classNamestringNo defaultMerged onto the <li>.

TimelineHeader

The event sentence and its time, on a wrapping line.

Other props spread onto <div>.

Props of TimelineHeader
PropTypeDefaultDescription
classNamestringNo defaultMerged after the defaults.

TimelineTime

The event's time, at the right of the header.

Other props spread onto <time>.

Props of TimelineTime
PropTypeDefaultDescription
dateTimestringNo defaultThe ISO date and time, for machines and screen readers.
classNamestringNo defaultMerged after the defaults.

TimelineBody

Content under the header: a note, a reply or an action.

Other props spread onto <div>.

Props of TimelineBody
PropTypeDefaultDescription
classNamestringNo defaultAdd 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.