Timeline
$ npx shadcn@latest add https://lacodda.github.io/dowel/r/timeline.jsonA release history, an audit trail, the steps a job went through. Four products draw one, and all four drew it the same way — a list with a border on the left and a dot positioned over it by hand. Three things go wrong with that version.
The rail runs past the last entry. A border on the container ends where the container does, which is below the last dot, so the history trails off into a line going nowhere and a finished list cannot be told from one still loading. Here the rail belongs to the entry, and the last one does not draw it — which also means an entry renders correctly on its own, without a container to inherit a border from.
It is a list, and says so. A stack of <div>s is announced as nothing; an
<ol> tells a reader how many entries there are and which one they are on,
which is the whole navigational value of a history.
The state is a colour. A red dot for a failed step is invisible to the
reader it matters most to. statusLabel names the state in the product’s
words, and an icon gives it a shape as well as a hue.
<Timeline> {events.map((event, index) => ( <TimelineItem key={event.id} title={t(`event.${event.kind}`)} time={<RelativeTime value={event.at} />} status={toneOf(event)} statusLabel={t(`event.state.${event.state}`)} last={index === events.length - 1} > {event.detail} </TimelineItem> ))}</Timeline>It does not sort. “Newest first” and “the order it happened” are both right, and only the product knows which it means.
An unnamed marker stays out of the tree. A dot meaning nothing beyond
“here is an entry” is decoration, and announcing it would be noise on top of
the title beside it. Give statusLabel whenever the colour carries something
the title does not already say.
pending is a step not taken yet: the ring of a dot with nothing in it,
which is how every other tool draws it.
Takes the same vocabulary as StatusDot, and
time takes a node so a product can pass its own
RelativeTime rather than a string it
formatted early.
Props
Read from the types in ui/timeline.tsx, the file a product copies - so the description of each prop is also what an editor shows over it there.
Timeline
The list itself.
| Prop | Type | Default | |
|---|---|---|---|
side | start | end | Draw the rail and the markers on the right of the text, for a right-to-left reading or a history beside a wider body of content. |
And every prop of React's HTMLAttributes<HTMLOListElement>, passed on.
TimelineItem
One entry: a marker, a rail down to the next, and what happened.
| Prop | Type | Default | |
|---|---|---|---|
status | accent | good | warn | bad | info | neutral | 'neutral' | |
pending | boolean | false | A step not taken yet: the ring of a dot with nothing in it, which is how every other tool draws "not done" and is worth borrowing. Its glyph is dim, whatever the status: the fill it was paired with is not there to read it against. |
titlerequired | ReactNode | The headline of the entry - what happened. | |
time | ReactNode | When it happened. Drawn quieter, and given as a node so the product can pass its own RelativeTime rather than a string it formatted early. | |
icon | ReactNode | A shape inside the marker - a tick, a cross, a number. The second channel, for a reader who does not separate the hues. | |
last | boolean | false | The last entry does not draw a rail below it. Set by the product, because only it knows whether the history ends here or merely stops being shown. |
statusLabel | string | Names the state in the product's words, for a reader who does not see the colour. Required whenever status carries meaning the title does not already say. |
And every prop of React's HTMLAttributes<HTMLLIElement> except title, passed on.
Accessibility
The examples on the stand, through axe 4.14.0 in a browser, in both themes, against WCAG 2.2 A and AA and axe's best practices: 18 rules applied and passed, and none failed. The accessibility report has every item.
History
Each release that changed ui/timeline.tsx - the file a product copies - newest first.