On this page
Timeline
Show a series of events or steps in chronological order.
- Overview
- Rules and guidance
- Properties
- Styles
- Accessibility
When to use
- To display a sequence of events in chronological order, for example a customer's account activity or the steps in a completed process
- To show the progress of a multi-step task where each step is discrete and has already happened
- When the time or sequence relationship between events is meaningful to the user
When not to use
- For future steps in an incomplete process - if the user has not yet completed the steps, use a progress indicator or stepper pattern instead.
- For navigation - a Timeline is a display component, not a navigation component. Use Tabs or page structure for navigable sections.
- For a simple list without meaningful sequence - use a List instead.
Anatomy
- Separator
- Node
- Content
Basic usage
Edit live code
<Timeline> <TimelineItem> <P><Strong>12 Jul 2023</Strong><br />You told us that you're moving out</P> </TimelineItem> <TimelineItem> <P><Strong>20 Jul 2023</Strong><br />Your moving day</P> </TimelineItem> <TimelineItem> <P><Strong>20 Jul 2023</Strong><br />Submit your final meter readings</P> </TimelineItem> </Timeline>
Variants and features
Custom node
By default each item uses a TimelineDot, a solid circle in colors.border. Replace it with any element using the node prop.
Custom nodes must have a background colour and position: relative so they appear in front of the separator line. All nodes in a timeline must be the same width to keep the content column visually aligned.
Edit live code
<Timeline> <TimelineItem node={ <Icon name="electricity" size="2em" style={{ position: 'relative', background: 'gold', padding: 4, borderRadius: 4 }}/> }> <P>Electricity supply connected</P> </TimelineItem> <TimelineItem node={ <Icon name="gas" size="2em" style={{ position: 'relative', background: 'deepskyblue', padding: 4, borderRadius: 4 }}/> }> <P>Gas supply connected</P> </TimelineItem> </Timeline>
Custom separator
By default each item uses a TimelineSeparator, a vertical line in colors.borderMuted. The separator on the last item is hidden automatically. Replace it using the separator prop.
When customising a separator, extend the default TimelineSeparator with a style override rather than replacing it entirely. This preserves the base positioning and last-item hiding behaviour.
The separator is a filled block, so override background rather than a border. Here the completed steps use a stronger colour than the default colors.borderMuted:
Edit live code
<Timeline> <TimelineItem separator={<TimelineSeparator style={{ background: '#07883F' }} />}> <P>Move-out date confirmed</P> </TimelineItem> <TimelineItem separator={<TimelineSeparator style={{ background: '#07883F' }} />}> <P>Final meter readings submitted</P> </TimelineItem> <TimelineItem> <P>Final bill issued</P> </TimelineItem> </Timeline>
usage
- Timeline must not be used for navigationTimeline is a read-only display component. It has no interactive affordances. Use Tabs, links, or page structure for navigable sections.Copy rule ID for timeline.not-for-navigation
- Timeline should only be used for content where the sequence or time relationship between items is meaningfulThe visual structure of the Timeline, connected nodes on a vertical axis, implies a meaningful order. Using it for unordered content misleads the user into inferring sequence or causality that does not exist.Copy rule ID for timeline.chronological-only
composition
- Timeline will hide the separator after the last TimelineItem automatically via CSS. Do not manually force it visibleForcing the last separator visible creates a dangling line that implies a continuation that does not exist.Copy rule ID for timeline.separator-hidden-automatically
- Timeline must use nodes of the same width across all TimelineItemsThe content column alignment depends on the node column being a consistent width. Mixed node widths shift the content column unpredictably across items.Copy rule ID for timeline.consistent-node-width
- If node prop is a custom elementTimelineItem node must have a background colour and position relative when a custom node is usedThe separator line is positioned behind the node using z-index layering. Without a background, the line shows through the custom node. Without position relative, the node renders behind the separator.Copy rule ID for timeline.custom-node-background
- If separator prop is a custom elementTimelineItem separator should extend the default TimelineSeparator rather than replacing it with an unrelated elementTimelineSeparator includes base CSS for positioning and automatic hiding on the last item. Replacing it entirely requires reimplementing this behaviour manually.Copy rule ID for timeline.custom-separator-extend
Timeline
Also accepts all props from HTMLAttributes, OlHTMLAttributes.
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
as | ElementType | — | No | Renders the component as a different element or component, e.g. `as="section"`. |
forwardedAs | ElementType | — | No | Like `as`, but passed on to the component being wrapped, for when a styled wrapper should keep its own element and change the one beneath it. |
TimelineItem
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
separator | ReactNode | <TimelineSeparator /> | No | Connecting line drawn from this item's node to the next. The default separator is hidden on the last item; a custom one is not. |
node | ReactNode | <TimelineDot /> | No | Marker shown at the start of the item, for example an icon in place of the default dot. |
children | string | number | boolean | ReactElement<any, string | JSXElementConstructor<any>> | ReactFragment | ReactPortal | — | No | — |
TimelineDot
Also accepts all props from HTMLAttributes.
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
as | ElementType | — | No | Renders the component as a different element or component, e.g. `as="section"`. |
forwardedAs | ElementType | — | No | Like `as`, but passed on to the component being wrapped, for when a styled wrapper should keep its own element and change the one beneath it. |
TimelineSeparator
Also accepts all props from HTMLAttributes.
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
as | ElementType | — | No | Renders the component as a different element or component, e.g. `as="section"`. |
forwardedAs | ElementType | — | No | Like `as`, but passed on to the component being wrapped, for when a styled wrapper should keep its own element and change the one beneath it. |
Styles
timeline
padding: 0;
timeline-list-item
display: flex;
position: relative;
timeline-separator
position: absolute;
left: 50%;
top: 0;
width: ${borderWidths.heavy};
transform: translateX(-50%);
background: ${colors.borderMuted};
height: 100%;
display: block;
margin-top: ${space[1]};
${TimelineListItem}:last-child & {
display: none;
}
timeline-node
position: relative;
margin-right: ${space[2]};
padding-bottom: ${space[3]};
${mediaQueries.smallAndUp} {
margin-right: ${space[3]};
padding-bottom: ${space[4]};
}
${TimelineListItem}:last-child & {
padding-bottom: 0;
}
timeline-dot
position: relative;
width: ${space[5]};
height: ${space[5]};
background: ${colors.border};
border-radius: 50%;
overflow: hidden;
flex-shrink: 0;
${mediaQueries.smallAndUp} {
width: ${space[6]};
height: ${space[6]};
}
timeline-item-content
display: flex;
flex-direction: column;
justify-content: center;
flex: 1;
padding-bottom: ${space[3]};
${mediaQueries.smallAndUp} {
padding-bottom: ${space[4]};
}
${TimelineListItem}:last-child & {
padding-bottom: 0;
}
In brief
Timeline is an ordered list. A screen reader announces the items in markup order, as list items. The dots and separators are empty elements and add nothing to what is announced.
What Nebula provides
- The Timeline is an ol and each TimelineItem an li, so the order and the number of items are in the markup.
- The dot and the separator hold no text, so a screen reader reads only the item content.
Accessible design
Write the date or time in the text of each item
The line and the dots show the order, but a screen reader only hears the content. "12 Jul 2023, You told us that you are moving out" says when it happened.
WCAG: 1.3.1 Info and Relationships
If a custom node carries meaning, say it in the item text too
An Icon used as a node is hidden from screen readers. An electricity icon beside "Electricity supply connected" is decoration, not the only copy.
WCAG: 1.1.1 Non-text Content 1.4.1 Use of Color
Accessible development
Put the items in chronological order in the markup
A screen reader reads the items in the order they are written. Reordering them with CSS changes what a sighted user sees and not what is announced.
WCAG: 1.3.2 Meaningful Sequence
Accessibility testing status
Reviewed on 9 October 2026 against v17.0.1.
| Test | Status | Tested with |
|---|---|---|
| Default state | Tested | axe-core via the Storybook test runner (CI) |
| Advanced states | Tested | axe-core via the Storybook test runner (CI) |
| Keyboard navigation | Manually tested | keyboard (reported by the owner) |
| Screen reader | Partially tested | VoiceOver with Chrome (reported by the owner) |
What these tests and statuses mean
The table records which checks were run on this component on its own. It is not a statement of WCAG conformance, and it does not cover how the component is used.
The tests
- Default state: The component as it first appears, before anything is clicked or changed.
- Advanced states: Every other state and variant: error, open, selected, each size and style, and hovering.
- Keyboard navigation: Whether every function can be reached and used with the keyboard alone, with a visible focus ring.
- Screen reader: Whether a screen reader announces what is on screen correctly: the name, the role and any state.
The statuses
- Tested: Every check in this row passed in an automated run that repeats on every pull request.
- Manually tested: Every check in this row was run once, by a person or with a tool they started, and all of them passed. It is not re-run on every change.
- Partially tested: Some checks in this row were run once and passed; the rest are still to do.
- Issue found: A check in this row failed. The ticket is shown beside the status.
- Not tested: No check in this row has been run yet.
- Not applicable: This row does not apply to the component, for the reason given beside it.