Skip to main content

Timeline

Events in order, vertical or horizontal, with indicators, dates and a completed state.

  1. Project created

    Repository initialised from the starter template.
  2. First deploy

    Preview environment went live on the staging domain.
  3. Design review

    Tokens and spacing approved by the design team.
  4. Public launch

    Production release scheduled after QA sign-off.
1'use client';
2
3import { Timeline } from '@/components/ui/timeline';
4
5const EVENTS = [
6 {
7 step: 1,
8 date: 'Mar 12',
9 title: 'Project created',
10 body: 'Repository initialised from the starter template.',
11 },
12 {
13 step: 2,
14 date: 'Mar 14',
15 title: 'First deploy',
16 body: 'Preview environment went live on the staging domain.',
17 },
18 {
19 step: 3,
20 date: 'Mar 20',
21 title: 'Design review',
22 body: 'Tokens and spacing approved by the design team.',
23 },
24 {
25 step: 4,
26 date: 'Mar 28',
27 title: 'Public launch',
28 body: 'Production release scheduled after QA sign-off.',
29 },
30];
31
32export function Default() {
33 return (
34 <Timeline value={2} className="max-w-md">
35 {EVENTS.map((event) => (
36 <Timeline.Item key={event.step} step={event.step}>
37 <Timeline.Header>
38 <Timeline.Date>{event.date}</Timeline.Date>
39 <Timeline.Title>{event.title}</Timeline.Title>
40 </Timeline.Header>
41 <Timeline.Indicator />
42 <Timeline.Separator />
43 <Timeline.Content>{event.body}</Timeline.Content>
44 </Timeline.Item>
45 ))}
46 </Timeline>
47 );
48}

Installation

pnpm dlx nachui add timeline

Anatomy

1import { Timeline } from '@/components/ui/timeline';
1<Timeline value={2}>
2 <Timeline.Item step={1}>
3 <Timeline.Header>
4 <Timeline.Date>Mar 12</Timeline.Date>
5 <Timeline.Title>Project created</Timeline.Title>
6 </Timeline.Header>
7 <Timeline.Indicator />
8 <Timeline.Separator />
9 <Timeline.Content>Repository initialised from the starter template.</Timeline.Content>
10 </Timeline.Item>
11</Timeline>
Every item declares its step. Items whose step is at or below the timeline value are completed: their indicator fills with the primary color and the separator leading out of them follows. The item at value also gets data-active, so you can style the current step on its own. The root renders an ordered list and each item a list item.

Composition

Use the following composition to build a Timeline:
Timeline
└── Timeline.Item
├── Timeline.Header
│ ├── Timeline.Date
│ └── Timeline.Title
├── Timeline.Indicator
├── Timeline.Separator
└── Timeline.Content

Variants

Horizontal

orientation="horizontal" lays the steps out left to right with the line above the content. Items share the width evenly.
  1. Ordered

  2. Packed

  3. Shipped

  4. Delivered

1'use client';
2
3import { Timeline } from '@/components/ui/timeline';
4
5const STAGES = [
6 { step: 1, date: 'Mon', title: 'Ordered' },
7 { step: 2, date: 'Tue', title: 'Packed' },
8 { step: 3, date: 'Wed', title: 'Shipped' },
9 { step: 4, date: 'Fri', title: 'Delivered' },
10];
11
12export function Horizontal() {
13 return (
14 <Timeline orientation="horizontal" value={3} className="w-full max-w-lg">
15 {STAGES.map((stage) => (
16 <Timeline.Item key={stage.step} step={stage.step}>
17 <Timeline.Header>
18 <Timeline.Date>{stage.date}</Timeline.Date>
19 <Timeline.Title>{stage.title}</Timeline.Title>
20 </Timeline.Header>
21 <Timeline.Indicator />
22 <Timeline.Separator />
23 </Timeline.Item>
24 ))}
25 </Timeline>
26 );
27}

Icons

Pass an icon as the indicator's children and size it with className. Add padding to the item so the content clears the larger marker.
  1. Commit pushed

    feat(ui): add a side-aware caret to the tooltip
  2. Checks passed

    Lint, type-check and 312 tests in 48s.
  3. Merged into main

    Squashed by lucia after one approval.
  4. Deployed

    Waiting for the production rollout window.
1'use client';
2
3import { GitCommitIcon, GitMergeIcon, Rocket01Icon, Tick02Icon } from '@hugeicons/core-free-icons';
4import { HugeiconsIcon } from '@hugeicons/react';
5import { Timeline } from '@/components/ui/timeline';
6
7const STEPS = [
8 {
9 step: 1,
10 icon: GitCommitIcon,
11 title: 'Commit pushed',
12 body: 'feat(ui): add a side-aware caret to the tooltip',
13 },
14 {
15 step: 2,
16 icon: Tick02Icon,
17 title: 'Checks passed',
18 body: 'Lint, type-check and 312 tests in 48s.',
19 },
20 {
21 step: 3,
22 icon: GitMergeIcon,
23 title: 'Merged into main',
24 body: 'Squashed by lucia after one approval.',
25 },
26 {
27 step: 4,
28 icon: Rocket01Icon,
29 title: 'Deployed',
30 body: 'Waiting for the production rollout window.',
31 },
32];
33
34export function Icons() {
35 return (
36 <Timeline value={3} className="max-w-md">
37 {STEPS.map((item) => (
38 <Timeline.Item key={item.step} step={item.step} className="ps-10">
39 <Timeline.Header>
40 <Timeline.Title>{item.title}</Timeline.Title>
41 </Timeline.Header>
42 <Timeline.Indicator className="bg-muted text-muted-foreground size-7 border-0 [&_svg]:size-4">
43 <HugeiconsIcon icon={item.icon} />
44 </Timeline.Indicator>
45 <Timeline.Separator />
46 <Timeline.Content>{item.body}</Timeline.Content>
47 </Timeline.Item>
48 ))}
49 </Timeline>
50 );
51}

Alternate

alternate mirrors every other item across the line, for roadmaps and history pages.
  1. Discovery

    Interviews with twelve teams and a first prototype.
  2. Private beta

    Fifty workspaces onboarded behind a feature flag.
  3. General availability

    Self-serve signup, billing and the public docs.
  4. Integrations

    Slack, Linear and GitHub apps in the marketplace.
1'use client';
2
3import { Timeline } from '@/components/ui/timeline';
4
5const MILESTONES = [
6 {
7 step: 1,
8 date: 'Q1',
9 title: 'Discovery',
10 body: 'Interviews with twelve teams and a first prototype.',
11 },
12 {
13 step: 2,
14 date: 'Q2',
15 title: 'Private beta',
16 body: 'Fifty workspaces onboarded behind a feature flag.',
17 },
18 {
19 step: 3,
20 date: 'Q3',
21 title: 'General availability',
22 body: 'Self-serve signup, billing and the public docs.',
23 },
24 {
25 step: 4,
26 date: 'Q4',
27 title: 'Integrations',
28 body: 'Slack, Linear and GitHub apps in the marketplace.',
29 },
30];
31
32export function Alternate() {
33 return (
34 <Timeline alternate value={2} className="w-full max-w-xl">
35 {MILESTONES.map((milestone) => (
36 <Timeline.Item key={milestone.step} step={milestone.step}>
37 <Timeline.Header>
38 <Timeline.Date>{milestone.date}</Timeline.Date>
39 <Timeline.Title>{milestone.title}</Timeline.Title>
40 </Timeline.Header>
41 <Timeline.Indicator />
42 <Timeline.Separator />
43 <Timeline.Content>{milestone.body}</Timeline.Content>
44 </Timeline.Item>
45 ))}
46 </Timeline>
47 );
48}

Left-aligned dates

Push the item in with a margin and position the date in the gutter. The pieces are plain elements, so a couple of classes are enough.
  1. Ticket opened

    Customer reported a broken checkout on Safari.
  2. Assigned to Nico

    Escalated to the payments squad.
  3. Fix deployed

    Patched the Apple Pay session handshake.
  4. Resolved

    Customer confirmed the order went through.
1'use client';
2
3import { Timeline } from '@/components/ui/timeline';
4
5const ACTIVITY = [
6 {
7 step: 1,
8 date: '09:12',
9 title: 'Ticket opened',
10 body: 'Customer reported a broken checkout on Safari.',
11 },
12 { step: 2, date: '09:40', title: 'Assigned to Nico', body: 'Escalated to the payments squad.' },
13 {
14 step: 3,
15 date: '11:05',
16 title: 'Fix deployed',
17 body: 'Patched the Apple Pay session handshake.',
18 },
19 { step: 4, date: '11:30', title: 'Resolved', body: 'Customer confirmed the order went through.' },
20];
21
22export function LeftDates() {
23 return (
24 <Timeline value={4} className="max-w-lg">
25 {ACTIVITY.map((entry) => (
26 <Timeline.Item key={entry.step} step={entry.step} className="ms-16">
27 <Timeline.Header>
28 <Timeline.Date className="absolute -start-16 top-0.5 w-14 text-end font-mono">
29 {entry.date}
30 </Timeline.Date>
31 <Timeline.Title>{entry.title}</Timeline.Title>
32 </Timeline.Header>
33 <Timeline.Indicator />
34 <Timeline.Separator />
35 <Timeline.Content>{entry.body}</Timeline.Content>
36 </Timeline.Item>
37 ))}
38 </Timeline>
39 );
40}

API Reference

Timeline

PropTypeDefaultDescription
valuenumber1Steps at or below this value are completed
orientation'vertical' | 'horizontal''vertical'Layout direction
alternatebooleanfalseMirror every other item across the line (vertical)
classNamestring-Additional CSS classes

Timeline.Item

PropTypeDefaultDescription
stepnumber-Position of the item, compared against value
classNamestring-Additional CSS classes
Exposes data-completed and data-active for styling.

Timeline.Title

PropTypeDefaultDescription
as'h2' | 'h3' | 'h4' | 'h5' | 'h6''h3'Heading level
classNamestring-Additional CSS classes

Timeline.Date

Renders a time element and accepts dateTime plus className.

Timeline.Header, Timeline.Indicator, Timeline.Separator, Timeline.Content

Layout parts of an item. Each accepts className and standard div props. The indicator takes children for a custom marker.
Found something to improve?

Notice a bug, typo, or missing detail on this page? Help us make the documentation better by opening a GitHub issue.

Create an Issue
Ctrl+I