Timeline
Events in order, with markers and a connecting line.
Example
Installation
pnpm dlx shadcn@latest add superzero11/uiset/timelineIt lands in components/ui/timeline.tsx, with UISet’s root (uiset). Wrap your app in <UISet> once, and every component takes its skin.
What UISet adds
The component is UI SDK’s own, with its parts, props and names, so everything below works as UI SDK documents it. UISet adds the look and checks it:
- Styles in five skins, each in light and dark, from CSS variables you can override.
- Text at 4.5:1 contrast and control edges and focus rings at 3:1, in every skin and theme, checked with axe in tests.
- Haptic cues on phones, and sound cues when you set
soundon<UISet>. - Examples that name every control, so agents can find it (see Agents below).
The API reference and accessibility notes below are UI SDK’s, from its docs for @ui-sdk/react (Apache-2.0). Timeline on ui-sdk.dev has more usage examples. Your coding agent can read the same page at node_modules/@ui-sdk/react/docs/components/timeline.md.
API Reference
Timeline
orientation?(TimelineOrientation): Which arrow keys move between events: up and down, or left and right.loop?(boolean): Whether arrow keys wrap from the last event to the first.feed?(boolean): Whether the events are a feed that loads more as people scroll, rather than a list. Each event becomes an article with its position, and Page Down and Page Up move between them from anywhere inside one.busy?(boolean): Whether a feed is loading or changing events, which setsaria-busyso screen readers wait until it's done. Only a feed reads it.controller?({ focus: (value?: string) => void; }): Focuses an event from anywhere. Create one withcreateTimelineController().debug?(boolean): Logs every state change with its reason to the console, in development builds.
TimelineItem
value?(string): Names the event for the controller'sfocus(). Generated when left out.status?(TimelineStatus): Where the event stands in a process. The"current"one setsaria-current="step"and takes the tab stop first.
TimelineMarker, TimelineConnector, TimelineTime, TimelineTitle and TimelineDescription take no props of their own.
State
What render functions and state-based class names receive on Timeline:
orientation(TimelineOrientation): Which way the events run.feed(boolean): Whether the events are a feed rather than a list.busy(boolean): Whether a feed is loading or changing events.
On TimelineItem and every part inside it:
status(TimelineStatus | undefined): Where the event stands in a process, orundefinedwhen it has no status.orientation(TimelineOrientation): Which way the events run.
Controller
focus(value?)((value?: string | undefined) => void): Moves focus to an event by itsvalue, or to the event that holds the tab stop.
Accessibility
A timeline is a list of listitems, named with aria-label. With feed, it follows the WAI-ARIA feed pattern: a feed of articles, each with aria-posinset and aria-setsize, and aria-busy while it loads. Each event is named by its title through aria-labelledby and described by its time and description through aria-describedby. The current event has aria-current="step".
Keyboard
The events have one tab stop, a roving focus. Tab enters on the current event, else the first, then returns to the event focused last. Markup rendered on the server holds the stop on the first event until it hydrates.
| Key | Action |
|---|---|
ArrowDown / ArrowUp | Move focus between events in a vertical timeline |
ArrowRight / ArrowLeft | Move focus between events in a horizontal timeline |
Home / End | Focus the first or last event |
PageDown / PageUp | In a feed, focus the next or previous article, from inside |
Tab | Move from an event to the links and buttons inside it |
Arrow keys move only from an event itself, so controls inside keep them. In right-to-left text, Left and Right swap.
Tested Behavior
Every line below is an automated test.
- A busy feed sets aria-busy.
A list of events
- The events are a named list of list items.
- Each event is named by its title and described by its time and description.
- Tab lands on the current event, then on the links inside it, then leaves.
- The current event says it's the current step.
- Each event and its parts show its status.
- ArrowDown and ArrowUp move between events.
- ArrowLeft and ArrowRight do nothing in a vertical list.
- Home and End move to the first and last events.
- Arrow keys wrap from the last event to the first.
- Arrow keys on a link inside an event leave focus on the link.
- Page Down does nothing in a list.
- Tab comes back to the event focused last.
- Only the event with the tab stop is in the tab order.
- The first and last events are marked.
- An event added at the top becomes the first.
- Events that change places move the first and last marks.
- Each event carries its place and the list how many there are.
- The tab stop follows the current step when it moves before focus enters.
- The controller moves focus to an event.
- Markers and connectors are hidden from assistive technology.
A list that doesn't loop
- Arrow keys stop at either end.
A horizontal list
- ArrowRight and ArrowLeft move between events.
- ArrowDown and ArrowUp do nothing.
A feed
- The events are a named feed of articles.
- Each article says where it sits and how many there are.
- An article added at the top moves every position.
- Articles moved inside a wrapper take their new places.
- Page Down and Page Up move between articles.
- Page Down from a link inside an article moves to the next article.
- Page Down on the last article stays there.
- The feed isn't busy until it's told.
Runs in React, Vue and web components.
Agents
UI SDK’s agent bridge finds this component by its accessible name: give it a visible label or an aria-label, and an agent can read its state and call its actions without screenshots. Building with AI agents shows how to connect one.
This is agent.snapshot() for the example above, read live from this page. Change something in the example, then read it again: the values follow.
[
{
"ref": "c8",
"component": "timeline",
"actions": [],
"label": "Deploy history",
"text": [
"Deployed to production",
"Version 2.4.1 is live.",
"Preview ready"
]
}
]