Timeline
A vertical timeline for activity feeds and status history.
Order placed
Payment authorized for €48.00.
Delivery attempt failed
Nobody was home. A second attempt is scheduled.
Out for delivery
Expected before 18:00.
Delivered
Installation
Usage
Composable parts rather than one configured widget, because activity rows never stay simple: one item needs an avatar, another an inline diff, another a button. You arrange them; the component owns the rail, the alignment, and the status colors.
import {
Timeline,
TimelineConnector,
TimelineContent,
TimelineDescription,
TimelineIndicator,
TimelineItem,
TimelineTime,
TimelineTitle,
} from "@/components/ui/timeline";
<Timeline>
<TimelineItem status="complete">
<TimelineConnector />
<TimelineIndicator />
<TimelineContent>
<TimelineTitle>Order placed</TimelineTitle>
<TimelineDescription>Payment authorized.</TimelineDescription>
<TimelineTime>2 hours ago</TimelineTime>
</TimelineContent>
</TimelineItem>
<TimelineItem status="current">
<TimelineConnector />
<TimelineIndicator />
<TimelineContent>
<TimelineTitle>In transit</TimelineTitle>
</TimelineContent>
</TimelineItem>
</Timeline>;Status
status on the item drives the indicator and the connector below it, and reaches them through
context — so nothing has to be threaded down by hand.
| Status | Reads as |
|---|---|
complete | Done. Filled indicator, solid rail. The default. |
current | Where things are now. Ringed, hollow indicator. |
pending | Not reached yet. Muted indicator, muted rail. |
error | Failed at this step. |
Indicators
An empty TimelineIndicator is a dot. Put anything inside it — a number, an icon, a tiny avatar —
and it centers automatically:
<TimelineIndicator>
<IconCheck className="size-3" />
</TimelineIndicator>
<TimelineIndicator className="size-7">
<img src={user.avatar} alt="" className="size-full rounded-full object-cover" />
</TimelineIndicator>Resizing the indicator does not move the rail, so mixed sizes still line up.
Connectors
TimelineConnector draws the line from an item to the next one and hides itself on the last item,
so you can render it in every row without special-casing the end of the list.
Timestamps
TimelineTime accepts a Date or an ISO string and writes a machine-readable dateTime attribute.
Pass your own text as children whenever the page is server-rendered:
<TimelineTime date={event.createdAt}>{formatRelative(event.createdAt)}</TimelineTime>The automatic fallback formats with the viewer's locale and timezone, which the server does not know — so it will differ between the server render and the client. Text you supply, or a useMounted guard, avoids that.
Accessibility
The timeline is an ordered list and each entry is a list item, which is what makes it navigable as a
sequence rather than a wall of text. Indicators and connectors are aria-hidden, so status must also
be present in the text — "Failed to deliver", not a red dot alone.