mwui
GitHub

Timeline

A vertical timeline for activity feeds and status history.

  1. Order placed

    Payment authorized for €48.00.

  2. Delivery attempt failed

    Nobody was home. A second attempt is scheduled.

  3. Out for delivery

    Expected before 18:00.

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

StatusReads as
completeDone. Filled indicator, solid rail. The default.
currentWhere things are now. Ringed, hollow indicator.
pendingNot reached yet. Muted indicator, muted rail.
errorFailed 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.