Skip to content
cennge-ui
GitHubv0.3.1, changelog

Timeline

Events in order, each with its time: a history of what happened, or the steps of something still under way.

cennge-ui releases

  1. v0.3.1
    README maps set in New York
  2. v0.3.0
    Charts, ThemeScript, ThemeSwitch
  3. v0.2.0
    Maps on MapLibre GL
  4. v0.1.1
    Date limits take null
  5. v0.1.0
    First release

payload-cache v1.10.0

  1. Build1m 12s
  2. Tests3m 40s
  3. PreviewLive
    Waits for someone to promote it
  4. Production, upcoming
  1. Without activeIndex, a timeline is a history: every item is done and takes the colour.
  2. Promote the deploy: activeIndex moves on, Production takes the colour and becomes the current step.
  3. Items past activeIndex stay muted, and screen readers hear them as upcoming.
Live: press the button.

Field marks

Import
import { Timeline, TimelineItem } from "cennge-ui";
Size
1.7 KB gzip for this import, its dependencies aside
Pattern
Native <ol>; a time with a dateTime is a <time>; the current step is aria-current="step"
State
None to hold: activeIndex says how far it has got
SSR
Rendered on the server in the Next.js check; hydrates without a warning
Horizontal
Scrolls inside itself when too wide, and takes Tab so the arrows scroll it
Tests
Unit tests; end-to-end in Chromium, Firefox and WebKit, on desktop and phone viewports: timeline

When to use#

Use a Timeline for events in the order they happened: releases, deploys, the life of an issue. With activeIndex it tracks something under way instead, colouring the steps that are done and muting those to come.

Each TimelineItem has a title, an optional time, and anything you put inside it as the description. The list is an <ol> and every item an <li>, so the order is in the markup, not only on the screen.

Similar#

  • Stepper is a form or a task in steps that people move through and click; a Timeline only shows where things stand.
  • Table suits events you compare by column or sort; a Timeline reads top to bottom.

Tracking progress#

activeIndex is the index of the current item. Items up to it are done and take the color; the current one is aria-current="step"; later ones are muted, and screen readers hear ", upcoming" after their title. lineVariant="dashed" draws the line to the next item dashed, here for a release that has no date yet.

  1. Reported
    Cached pages keep their old TTL after a config change
  2. Reproduced
    A failing test on the fix/ttl branch
  3. Fix merged
    Waiting for the next release
  4. Released, upcoming
progress.tsx
import { Timeline, TimelineItem } from "cennge-ui";

export default function IssueProgress() {
  return (
    <Timeline aria-label="Progress of the payload-cache issue" activeIndex={2} className="w-full max-w-80">
      <TimelineItem title="Reported" time="Nov 2" dateTime="2026-11-02">
        Cached pages keep their old TTL after a config change
      </TimelineItem>
      <TimelineItem title="Reproduced" time="Nov 3" dateTime="2026-11-03">
        A failing test on the fix/ttl branch
      </TimelineItem>
      <TimelineItem title="Fix merged" time="Nov 5" dateTime="2026-11-05" lineVariant="dashed">
        Waiting for the next release
      </TimelineItem>
      <TimelineItem title="Released" />
    </Timeline>
  );
}

Colours, icons and plans#

color on an item overrides the timeline's, so a failed deploy can be red in a history of green ones. bullet puts an icon in the dot. Give an item a dateTime beside its time and the time becomes a <time> element that machines can read; text such as "Next week" without one stays a <span>.

  1. v1.11.0 plannedNext week
  2. v1.10.0 deployed
    Production, in 84 seconds
  3. Deploy failed
    Typecheck exited with code 2
  4. v1.9.2 deployed
    Production, in 79 seconds
history.tsx
import { Timeline, TimelineItem } from "cennge-ui";

function CheckIcon() {
  return (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" aria-hidden="true">
      <path d="M5 12.5l4.5 4.5L19 7.5" strokeLinecap="round" strokeLinejoin="round" />
    </svg>
  );
}

function CrossIcon() {
  return (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" aria-hidden="true">
      <path d="M7 7l10 10M17 7 7 17" strokeLinecap="round" />
    </svg>
  );
}

export default function DeployHistory() {
  return (
    <Timeline aria-label="Deploys of payload-cache" className="w-full max-w-80">
      <TimelineItem title="v1.11.0 planned" time="Next week" color="default" lineVariant="dashed" />
      <TimelineItem title="v1.10.0 deployed" time="Nov 6, 09:12" dateTime="2026-11-06T09:12Z" color="success" bullet={<CheckIcon />}>
        Production, in 84 seconds
      </TimelineItem>
      <TimelineItem title="Deploy failed" time="Nov 5, 17:40" dateTime="2026-11-05T17:40Z" color="danger" bullet={<CrossIcon />}>
        Typecheck exited with code 2
      </TimelineItem>
      <TimelineItem title="v1.9.2 deployed" time="Oct 29, 13:27" dateTime="2026-10-29T13:27Z" color="success" bullet={<CheckIcon />}>
        Production, in 79 seconds
      </TimelineItem>
    </Timeline>
  );
}

Horizontal#

orientation="horizontal" lays the items out in a row, each at least 11rem wide. When they do not fit, as on a phone, the row scrolls inside itself rather than widening the page, and it is in the tab order so the arrow keys can scroll it.

  1. Version PR
    Opened by changesets/action
  2. Merged
    Pull request #4
  3. Published
    cennge-ui@0.3.1 on npm
horizontal.tsx
import { Timeline, TimelineItem } from "cennge-ui";

export default function ReleaseFlow() {
  return (
    <Timeline aria-label="How cennge-ui 0.3.1 was released" orientation="horizontal" className="w-full">
      <TimelineItem title="Version PR" time="Oct 31" dateTime="2026-10-31T23:27+02:00">
        Opened by changesets/action
      </TimelineItem>
      <TimelineItem title="Merged" time="Nov 1" dateTime="2026-11-01T10:01+02:00">
        Pull request #4
      </TimelineItem>
      <TimelineItem title="Published" time="Nov 1" dateTime="2026-11-01T10:40+02:00">
        cennge-ui@0.3.1 on npm
      </TimelineItem>
    </Timeline>
  );
}

Accessibility#

  • Name the timeline with aria-label or aria-labelledby; it is a list, and screen readers say how many items it holds.
  • Bullets and lines are decoration, hidden from screen readers. The state is in the text: aria-current="step" on the current item and ", upcoming" after the titles to come. upcomingLabel changes that word, e.g. for another language.
  • A horizontal timeline has tabIndex={0} whether or not it overflows, so it is always one Tab stop.

Props#

Timeline#

PropTypeDefault
orientation
"horizontal""vertical"
"vertical"
activeIndex
numberProgress, for tracking flows ("ordered → shipped → delivered"): items up to this index are done and coloured, the item at it is aria-current, later items are muted. Omit for a plain history.
–
color
"default""primary""secondary""success""warning""danger""info"
"primary"
size
"sm""md""lg"
"md"
upcomingLabel
stringRead after the title of items past activeIndex.
"upcoming"

Also takes every attribute of <olist>.

TimelineItem#

PropTypeDefault
titlerequired
React.ReactNode
–
time
React.ReactNodeWhen it happened: "2 hours ago", "Oct 18".
–
dateTime
stringMachine-readable time for the <time> element, e.g. "2026-10-18T14:30".
–
bullet
React.ReactNodeIcon inside the bullet.
–
color
"default""primary""secondary""success""warning""danger""info"Overrides the timeline colour for this item, e.g. danger for a failed deploy.
–
lineVariant
"solid""dashed"Style of the line from this item to the next. Dashed suits planned events.
"solid"

Also takes every attribute of <li>, except title, color, which mean something else here.