# Timeline

> A timeline on Vercel's UI SDK for activity, order progress and agent steps, vertical or horizontal, with statuses, timestamps, icon markers and a feed mode.

Canonical: https://hextaui.com/docs/ui-sdk-timeline
Last updated: 2026-11-14

> **Beta.** Built on Vercel’s UI SDK 0.1.0-beta.2 (`@ui-sdk/react`), a public beta. Its API may change.

```tsx title="components/examples/ui-sdk-timeline/demo.tsx"
import {
  Timeline,
  TimelineConnector,
  TimelineDescription,
  TimelineItem,
  TimelineMarker,
  TimelineTime,
  TimelineTitle,
} from "@/components/ui/ui-sdk-timeline"

const events = [
  {
    title: "Deployed to production",
    dateTime: "2026-11-10T14:32",
    time: "Today, 2:32 PM",
    description: "Version 2.4.1 is live in every region.",
  },
  {
    title: "Checks passed",
    dateTime: "2026-11-10T14:20",
    time: "Today, 2:20 PM",
    description: "312 tests and the bundle size budget.",
  },
  {
    title: "Pull request merged",
    dateTime: "2026-11-10T13:58",
    time: "Today, 1:58 PM",
    description: "Ada merged Cache search results into main.",
  },
  {
    title: "Review requested",
    dateTime: "2026-11-09T17:05",
    time: "Yesterday, 5:05 PM",
    description: "Grace and Linus were asked to review.",
  },
]

function UiSdkTimelineDemo() {
  return (
    <Timeline aria-label="Activity" className="w-full max-w-sm">
      {events.map((event) => (
        <TimelineItem key={event.title}>
          <TimelineMarker />
          <TimelineConnector />
          <TimelineTitle>{event.title}</TimelineTitle>
          <TimelineTime dateTime={event.dateTime}>{event.time}</TimelineTime>
          <TimelineDescription>{event.description}</TimelineDescription>
        </TimelineItem>
      ))}
    </Timeline>
  )
}

export { UiSdkTimelineDemo }
```

## Installation

### CLI

```bash
npx shadcn@latest add https://hextaui.com/r/ui-sdk-timeline.json
```

This adds the component, the HextaUI theme tokens and any HextaUI components it depends on.

### Manual

Add the HextaUI theme tokens (https://hextaui.com/docs/installation#theme) to your global CSS if you haven't yet, then install the dependencies.

```bash
pnpm add @ui-sdk/react@0.1.0-beta.2 @ui-sdk/core@0.1.0-beta.2 class-variance-authority cn
```

Copy and paste the following code into your project.

```tsx title="components/ui/ui-sdk-timeline.tsx"
"use client"

import * as React from "react"
import * as TimelinePrimitive from "@ui-sdk/react/timeline"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "cn"

type ClassName<State> =
  string | ((state: State) => string | undefined) | undefined

function mergeClassName<State>(base: string, className: ClassName<State>) {
  return typeof className === "function"
    ? (state: State) => cn(base, className(state))
    : cn(base, className)
}

function Timeline({
  className,
  ...props
}: React.ComponentProps<typeof TimelinePrimitive.Timeline>) {
  return (
    <TimelinePrimitive.Timeline
      className={mergeClassName(
        "group/timeline flex min-w-0 gap-(--timeline-gap) [--timeline-gap:--spacing(6)] [--timeline-marker:--spacing(5)] data-[orientation=horizontal]:-m-2 data-[orientation=horizontal]:flex-row data-[orientation=horizontal]:overflow-x-auto data-[orientation=horizontal]:overscroll-x-contain data-[orientation=horizontal]:p-2 data-[orientation=horizontal]:[--timeline-gap:--spacing(4)] data-[orientation=vertical]:flex-col",
        className
      )}
      {...props}
    />
  )
}

function TimelineItem({
  className,
  ...props
}: React.ComponentProps<typeof TimelinePrimitive.TimelineItem>) {
  return (
    <TimelinePrimitive.TimelineItem
      className={mergeClassName(
        "group/timeline-item relative grid min-w-0 content-start gap-x-3 gap-y-0.5 rounded-sm outline-none focus-visible:ring-3 focus-visible:ring-focus-ring focus-visible:ring-offset-4 focus-visible:ring-offset-background data-[orientation=horizontal]:min-w-28 data-[orientation=horizontal]:flex-1 data-[orientation=horizontal]:grid-cols-1 data-[orientation=vertical]:grid-cols-[var(--timeline-marker)_minmax(0,1fr)] data-[orientation=vertical]:[&>:not([data-slot=timeline-marker],[data-slot=timeline-connector])]:col-start-2",
        className
      )}
      {...props}
    />
  )
}

const timelineMarkerVariants = cva(
  "relative z-1 col-start-1 row-start-1 flex size-(--timeline-marker) shrink-0 items-center justify-center overflow-hidden rounded-full text-muted-foreground transition-[background-color,color,box-shadow] duration-200 ease-out-quint not-empty:bg-muted not-empty:text-foreground not-empty:inset-ring-(length:--hairline) not-empty:inset-ring-border group-data-[orientation=horizontal]/timeline:mb-2 empty:overflow-visible empty:before:size-2 empty:before:rounded-full empty:before:bg-muted-foreground/45 empty:before:transition-[background-color,box-shadow] empty:before:duration-200 data-complete:not-empty:bg-primary data-complete:not-empty:text-primary-foreground data-complete:not-empty:inset-ring-0 data-complete:empty:before:bg-foreground data-current:not-empty:bg-background data-current:not-empty:inset-ring-foreground/60 data-current:empty:before:bg-foreground data-current:empty:before:ring-4 data-current:empty:before:ring-foreground/10 data-upcoming:not-empty:bg-transparent data-upcoming:not-empty:text-muted-foreground data-upcoming:empty:before:bg-transparent data-upcoming:empty:before:inset-ring-[1.5px] data-upcoming:empty:before:inset-ring-muted-foreground/45 motion-reduce:transition-none forced-colors:border forced-colors:empty:before:bg-[CanvasText] [&_img]:size-full [&_img]:object-cover [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
  {
    variants: {
      variant: {
        default: "",
        success:
          "not-empty:bg-success/12 not-empty:text-success not-empty:inset-ring-success/25 empty:before:bg-success data-complete:not-empty:bg-success data-complete:not-empty:text-success-foreground data-complete:empty:before:bg-success",
        warning:
          "not-empty:bg-warning/12 not-empty:text-warning not-empty:inset-ring-warning/25 empty:before:bg-warning data-complete:not-empty:bg-warning data-complete:not-empty:text-warning-foreground data-complete:empty:before:bg-warning",
        destructive:
          "not-empty:bg-destructive/12 not-empty:text-destructive not-empty:inset-ring-destructive/25 empty:before:bg-destructive data-complete:not-empty:bg-destructive data-complete:not-empty:text-destructive-foreground data-complete:empty:before:bg-destructive",
        info: "not-empty:bg-info/12 not-empty:text-info not-empty:inset-ring-info/25 empty:before:bg-info data-complete:not-empty:bg-info data-complete:not-empty:text-info-foreground data-complete:empty:before:bg-info",
      },
    },
    defaultVariants: {
      variant: "default",
    },
  }
)

function TimelineMarker({
  className,
  variant = "default",
  ...props
}: React.ComponentProps<typeof TimelinePrimitive.TimelineMarker> &
  VariantProps<typeof timelineMarkerVariants>) {
  return (
    <TimelinePrimitive.TimelineMarker
      data-variant={variant}
      className={mergeClassName(timelineMarkerVariants({ variant }), className)}
      {...props}
    />
  )
}

function TimelineConnector({
  className,
  ...props
}: React.ComponentProps<typeof TimelinePrimitive.TimelineConnector>) {
  return (
    <TimelinePrimitive.TimelineConnector
      className={mergeClassName(
        "pointer-events-none absolute bg-border group-last/timeline-item:hidden group-data-last/timeline-item:hidden data-complete:bg-foreground/50 data-[orientation=horizontal]:start-[calc(var(--timeline-marker)+--spacing(1))] data-[orientation=horizontal]:end-[calc(--spacing(1)-var(--timeline-gap))] data-[orientation=horizontal]:top-[calc(var(--timeline-marker)/2-0.5px)] data-[orientation=horizontal]:h-px data-[orientation=vertical]:start-[calc(var(--timeline-marker)/2-0.5px)] data-[orientation=vertical]:top-[calc(var(--timeline-marker)+--spacing(1))] data-[orientation=vertical]:bottom-[calc(--spacing(1)-var(--timeline-gap))] data-[orientation=vertical]:w-px forced-colors:bg-[CanvasText]",
        className
      )}
      {...props}
    />
  )
}

function TimelineTitle({
  className,
  ...props
}: React.ComponentProps<typeof TimelinePrimitive.TimelineTitle>) {
  return (
    <TimelinePrimitive.TimelineTitle
      className={mergeClassName(
        "min-w-0 text-sm/5 font-medium text-foreground data-upcoming:text-muted-foreground",
        className
      )}
      {...props}
    />
  )
}

function TimelineTime({
  className,
  ...props
}: React.ComponentProps<typeof TimelinePrimitive.TimelineTime>) {
  return (
    <TimelinePrimitive.TimelineTime
      className={mergeClassName(
        "text-xs/5 text-muted-foreground tabular-nums",
        className
      )}
      {...props}
    />
  )
}

function TimelineDescription({
  className,
  ...props
}: React.ComponentProps<typeof TimelinePrimitive.TimelineDescription>) {
  return (
    <TimelinePrimitive.TimelineDescription
      className={mergeClassName(
        "min-w-0 text-sm text-pretty text-muted-foreground",
        className
      )}
      {...props}
    />
  )
}

const createTimelineController = TimelinePrimitive.createTimelineController

export {
  Timeline,
  TimelineItem,
  TimelineMarker,
  TimelineConnector,
  TimelineTitle,
  TimelineTime,
  TimelineDescription,
  createTimelineController,
  timelineMarkerVariants,
}
export type {
  TimelineController,
  TimelineOrientation,
  TimelineState,
  TimelineItemState,
  TimelineStatus,
} from "@ui-sdk/react/timeline"
```

Update the import paths to match your project setup.

## Usage

UI SDK adds a little CSS of its own in a cascade layer named `ui-sdk`. Declare that layer at the top of your global stylesheet, above the Tailwind import, so your classes win over it.

```css
@layer ui-sdk;

@import "tailwindcss";
```

```tsx
import {
  Timeline,
  TimelineConnector,
  TimelineDescription,
  TimelineItem,
  TimelineMarker,
  TimelineTime,
  TimelineTitle,
} from "@/components/ui/ui-sdk-timeline"
```

```tsx
<Timeline aria-label="Activity">
  <TimelineItem>
    <TimelineMarker />
    <TimelineConnector />
    <TimelineTitle>Deployed to production</TimelineTitle>
    <TimelineTime dateTime="2026-11-10T14:32">2:32 PM</TimelineTime>
    <TimelineDescription>Version 2.4.1 is live.</TimelineDescription>
  </TimelineItem>
</Timeline>
```

A timeline lists things that happened, or will, like an activity feed, an order’s progress or an agent’s steps. It’s a list for screen readers with one tab stop, and the arrow keys move between events. Pass `dateTime` to `TimelineTime` so the date is machine readable while the text stays friendly. UI SDK marks it experimental in 0.1.0-beta.2.

## Composition

```text
Timeline
├── TimelineItem
│   ├── TimelineMarker
│   ├── TimelineConnector
│   ├── TimelineTitle
│   ├── TimelineTime
│   └── TimelineDescription
└── TimelineItem
```

Every part inside an item is optional. The marker sits in its own column and the connector runs from it to the next event, hidden after the last one. Anything else you put in an item, like a button or a collapsible, lines up with the title.

## Examples

### Status

Give each item a `status` of `complete`, `current` or `upcoming`. The current event gets `aria-current="step"` and the tab stop, and a controller focuses an event by its `value`. Say the status in the title too, since the marker only shows it.

```tsx title="components/examples/ui-sdk-timeline/status.tsx"
"use client"

import * as React from "react"
import { IconCheck } from "@tabler/icons-react"

import { Button } from "@/components/ui/button"
import {
  Timeline,
  TimelineConnector,
  TimelineDescription,
  TimelineItem,
  TimelineMarker,
  TimelineTime,
  TimelineTitle,
  createTimelineController,
  type TimelineStatus,
} from "@/components/ui/ui-sdk-timeline"

const steps: {
  value: string
  title: string
  dateTime: string
  time: string
  description: string
  status: TimelineStatus
}[] = [
  {
    value: "placed",
    title: "Order placed",
    dateTime: "2026-11-01",
    time: "Nov 1",
    description: "We received your order.",
    status: "complete",
  },
  {
    value: "packed",
    title: "Packed",
    dateTime: "2026-11-02",
    time: "Nov 2",
    description: "Your items left the shelf.",
    status: "complete",
  },
  {
    value: "shipped",
    title: "Shipped, in transit",
    dateTime: "2026-11-03",
    time: "Nov 3",
    description: "On its way with the carrier.",
    status: "current",
  },
  {
    value: "delivered",
    title: "Delivered",
    dateTime: "2026-11-05",
    time: "Expected Nov 5",
    description: "Expected by Thursday.",
    status: "upcoming",
  },
]

function UiSdkTimelineStatus() {
  const [controller] = React.useState(createTimelineController)

  return (
    <div className="flex w-full max-w-sm flex-col gap-4">
      <Button
        size="sm"
        variant="outline"
        className="self-start"
        onClick={() => controller.focus("shipped")}
      >
        Go to current step
      </Button>
      <Timeline aria-label="Order status" controller={controller}>
        {steps.map((step) => (
          <TimelineItem
            key={step.value}
            value={step.value}
            status={step.status}
          >
            <TimelineMarker>
              {step.status === "complete" && <IconCheck aria-hidden="true" />}
            </TimelineMarker>
            <TimelineConnector />
            <TimelineTitle>{step.title}</TimelineTitle>
            <TimelineTime dateTime={step.dateTime}>{step.time}</TimelineTime>
            <TimelineDescription>{step.description}</TimelineDescription>
          </TimelineItem>
        ))}
      </Timeline>
    </div>
  )
}

export { UiSdkTimelineStatus }
```

### Agent steps

An icon in `TimelineMarker` turns it into a small circle. Put a spinner in the current step while an agent works, and a duration like `PT5S` in `dateTime`.

```tsx title="components/examples/ui-sdk-timeline/agent.tsx"
import {
  IconCheck,
  IconFileText,
  IconGitPullRequest,
  IconWorld,
} from "@tabler/icons-react"

import { Spinner } from "@/components/ui/spinner"
import {
  Timeline,
  TimelineConnector,
  TimelineDescription,
  TimelineItem,
  TimelineMarker,
  TimelineTime,
  TimelineTitle,
} from "@/components/ui/ui-sdk-timeline"

function UiSdkTimelineAgent() {
  return (
    <Timeline aria-label="Agent run" className="w-full max-w-sm">
      <TimelineItem status="complete">
        <TimelineMarker>
          <IconFileText aria-hidden="true" />
        </TimelineMarker>
        <TimelineConnector />
        <TimelineTitle>Read 4 files</TimelineTitle>
        <TimelineTime dateTime="PT2S">2s</TimelineTime>
        <TimelineDescription>
          search.ts, cache.ts, cache.test.ts and README.md
        </TimelineDescription>
      </TimelineItem>
      <TimelineItem status="complete">
        <TimelineMarker>
          <IconWorld aria-hidden="true" />
        </TimelineMarker>
        <TimelineConnector />
        <TimelineTitle>Searched the web</TimelineTitle>
        <TimelineTime dateTime="PT5S">5s</TimelineTime>
        <TimelineDescription>
          3 results for “LRU cache eviction in edge runtimes”
        </TimelineDescription>
      </TimelineItem>
      <TimelineItem status="current">
        <TimelineMarker>
          <Spinner size="sm" aria-hidden="true" />
        </TimelineMarker>
        <TimelineConnector />
        <TimelineTitle>Writing the fix, in progress</TimelineTitle>
        <TimelineDescription>
          Evicting the oldest entry once the cache holds 500 results.
        </TimelineDescription>
      </TimelineItem>
      <TimelineItem status="upcoming">
        <TimelineMarker>
          <IconGitPullRequest aria-hidden="true" />
        </TimelineMarker>
        <TimelineConnector />
        <TimelineTitle>Open a pull request</TimelineTitle>
      </TimelineItem>
      <TimelineItem status="upcoming">
        <TimelineMarker>
          <IconCheck aria-hidden="true" />
        </TimelineMarker>
        <TimelineConnector />
        <TimelineTitle>Run the checks</TimelineTitle>
      </TimelineItem>
    </Timeline>
  )
}

export { UiSdkTimelineAgent }
```

### Custom markers

The marker’s `variant` is `success`, `warning`, `destructive` or `info`, for an empty dot or an icon.

```tsx title="components/examples/ui-sdk-timeline/markers.tsx"
import {
  IconAlertTriangle,
  IconCheck,
  IconInfoCircle,
  IconRocket,
  IconX,
} from "@tabler/icons-react"

import {
  Timeline,
  TimelineConnector,
  TimelineDescription,
  TimelineItem,
  TimelineMarker,
  TimelineTime,
  TimelineTitle,
} from "@/components/ui/ui-sdk-timeline"

function UiSdkTimelineMarkers() {
  return (
    <Timeline aria-label="Deploy history" className="w-full max-w-sm">
      <TimelineItem>
        <TimelineMarker variant="success">
          <IconCheck aria-hidden="true" />
        </TimelineMarker>
        <TimelineConnector />
        <TimelineTitle>Deploy succeeded</TimelineTitle>
        <TimelineTime dateTime="2026-11-12T09:41">Nov 12, 9:41 AM</TimelineTime>
      </TimelineItem>
      <TimelineItem>
        <TimelineMarker variant="warning">
          <IconAlertTriangle aria-hidden="true" />
        </TimelineMarker>
        <TimelineConnector />
        <TimelineTitle>Slow build warning</TimelineTitle>
        <TimelineTime dateTime="2026-11-12T09:37">Nov 12, 9:37 AM</TimelineTime>
        <TimelineDescription>
          The build took 6 minutes, 2 over budget.
        </TimelineDescription>
      </TimelineItem>
      <TimelineItem>
        <TimelineMarker variant="destructive">
          <IconX aria-hidden="true" />
        </TimelineMarker>
        <TimelineConnector />
        <TimelineTitle>Deploy failed</TimelineTitle>
        <TimelineTime dateTime="2026-11-11T18:02">Nov 11, 6:02 PM</TimelineTime>
        <TimelineDescription>
          A migration timed out and was rolled back.
        </TimelineDescription>
      </TimelineItem>
      <TimelineItem>
        <TimelineMarker variant="info">
          <IconInfoCircle aria-hidden="true" />
        </TimelineMarker>
        <TimelineConnector />
        <TimelineTitle>Maintenance window set</TimelineTitle>
        <TimelineTime dateTime="2026-11-11T12:00">
          Nov 11, 12:00 PM
        </TimelineTime>
      </TimelineItem>
      <TimelineItem>
        <TimelineMarker>
          <IconRocket aria-hidden="true" />
        </TimelineMarker>
        <TimelineConnector />
        <TimelineTitle>Project created</TimelineTitle>
        <TimelineTime dateTime="2026-11-01T10:15">Nov 1, 10:15 AM</TimelineTime>
      </TimelineItem>
    </Timeline>
  )
}

export { UiSdkTimelineMarkers }
```

### Horizontal

`orientation="horizontal"` lays the events in a row, like a roadmap. Left and Right move between them, and `loop={false}` stops at the ends.

```tsx title="components/examples/ui-sdk-timeline/horizontal.tsx"
import { IconCheck } from "@tabler/icons-react"

import {
  Timeline,
  TimelineConnector,
  TimelineItem,
  TimelineMarker,
  TimelineTime,
  TimelineTitle,
  type TimelineStatus,
} from "@/components/ui/ui-sdk-timeline"

const milestones: {
  title: string
  dateTime: string
  time: string
  status: TimelineStatus
}[] = [
  {
    title: "Kickoff",
    dateTime: "2026-01",
    time: "January",
    status: "complete",
  },
  {
    title: "Design review",
    dateTime: "2026-03",
    time: "March",
    status: "complete",
  },
  { title: "Beta", dateTime: "2026-06", time: "June", status: "current" },
  {
    title: "Launch",
    dateTime: "2026-09",
    time: "September",
    status: "upcoming",
  },
]

function UiSdkTimelineHorizontal() {
  return (
    <Timeline
      orientation="horizontal"
      loop={false}
      aria-label="Roadmap"
      className="w-full max-w-xl"
    >
      {milestones.map((milestone) => (
        <TimelineItem key={milestone.title} status={milestone.status}>
          <TimelineMarker>
            {milestone.status === "complete" && (
              <IconCheck aria-hidden="true" />
            )}
          </TimelineMarker>
          <TimelineConnector />
          <TimelineTitle>{milestone.title}</TimelineTitle>
          <TimelineTime dateTime={milestone.dateTime}>
            {milestone.time}
          </TimelineTime>
        </TimelineItem>
      ))}
    </Timeline>
  )
}

export { UiSdkTimelineHorizontal }
```

### Details

Links and buttons inside an event stay in the tab order. Tab moves from the event to them, and the arrow keys still move between events.

```tsx title="components/examples/ui-sdk-timeline/details.tsx"
import { Button } from "@/components/ui/button"
import {
  Collapsible,
  CollapsibleContent,
  CollapsibleTrigger,
  CollapsibleTriggerIcon,
} from "@/components/ui/collapsible"
import {
  Timeline,
  TimelineConnector,
  TimelineDescription,
  TimelineItem,
  TimelineMarker,
  TimelineTime,
  TimelineTitle,
} from "@/components/ui/ui-sdk-timeline"

const releases = [
  {
    title: "Version 2.4",
    dateTime: "2026-10-28",
    time: "October 28",
    description: "Faster search and resumable uploads.",
    changes: [
      "Search now matches file contents, not only names.",
      "Uploads resume after the connection drops.",
    ],
  },
  {
    title: "Version 2.3",
    dateTime: "2026-09-30",
    time: "September 30",
    description: "Shared links and a new sidebar.",
    changes: [
      "Shared links remember the folder they were opened from.",
      "The sidebar collapses to icons on narrow screens.",
    ],
  },
  {
    title: "Version 2.2",
    dateTime: "2026-08-26",
    time: "August 26",
    description: "Dark mode follows your system.",
    changes: ["Dark mode follows the system setting.", "Avatars are sharper."],
  },
]

function UiSdkTimelineDetails() {
  return (
    <Timeline aria-label="Changelog" className="w-full max-w-sm">
      {releases.map((release) => (
        <TimelineItem key={release.title}>
          <TimelineMarker />
          <TimelineConnector />
          <TimelineTitle>{release.title}</TimelineTitle>
          <TimelineTime dateTime={release.dateTime}>
            {release.time}
          </TimelineTime>
          <TimelineDescription>{release.description}</TimelineDescription>
          <Collapsible className="flex flex-col items-start gap-1 pt-1">
            <CollapsibleTrigger
              render={<Button variant="ghost" size="sm" />}
              className="-ms-2"
            >
              What changed
              <CollapsibleTriggerIcon data-icon="inline-end" />
            </CollapsibleTrigger>
            <CollapsibleContent>
              <ul className="m-0 flex list-disc flex-col gap-1 ps-4 text-sm text-muted-foreground">
                {release.changes.map((change) => (
                  <li key={change}>{change}</li>
                ))}
              </ul>
            </CollapsibleContent>
          </Collapsible>
        </TimelineItem>
      ))}
    </Timeline>
  )
}

export { UiSdkTimelineDetails }
```

### Feed

`feed` turns the list into a feed of articles for timelines that grow, and Page Down and Page Up move between them. Set `busy` while more load.

```tsx title="components/examples/ui-sdk-timeline/feed.tsx"
"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import {
  Timeline,
  TimelineConnector,
  TimelineDescription,
  TimelineItem,
  TimelineMarker,
  TimelineTime,
  TimelineTitle,
} from "@/components/ui/ui-sdk-timeline"

const updates = [
  {
    id: 1,
    author: "Ada",
    date: "2026-11-10",
    time: "Nov 10",
    text: "Shipped the new onboarding flow.",
  },
  {
    id: 2,
    author: "Grace",
    date: "2026-11-10",
    time: "Nov 10",
    text: "Fixed the flaky upload test.",
  },
  {
    id: 3,
    author: "Linus",
    date: "2026-11-09",
    time: "Nov 9",
    text: "Moved the docs to the new theme.",
  },
  {
    id: 4,
    author: "Margaret",
    date: "2026-11-09",
    time: "Nov 9",
    text: "Wrote the launch checklist.",
  },
  {
    id: 5,
    author: "Ada",
    date: "2026-11-08",
    time: "Nov 8",
    text: "Cut search latency in half.",
  },
  {
    id: 6,
    author: "Grace",
    date: "2026-11-07",
    time: "Nov 7",
    text: "Added keyboard shortcuts to the editor.",
  },
  {
    id: 7,
    author: "Linus",
    date: "2026-11-06",
    time: "Nov 6",
    text: "Cleaned up the old billing code.",
  },
  {
    id: 8,
    author: "Margaret",
    date: "2026-11-05",
    time: "Nov 5",
    text: "Ran the accessibility review.",
  },
  {
    id: 9,
    author: "Ada",
    date: "2026-11-04",
    time: "Nov 4",
    text: "Started the mobile layout.",
  },
]

const pageSize = 3

function UiSdkTimelineFeed() {
  const [count, setCount] = React.useState(pageSize)
  const [busy, setBusy] = React.useState(false)
  const timer = React.useRef<ReturnType<typeof setTimeout>>(undefined)

  React.useEffect(() => () => clearTimeout(timer.current), [])

  return (
    <div className="flex w-full max-w-sm flex-col gap-4">
      <Timeline feed busy={busy} aria-label="Team updates">
        {updates.slice(0, count).map((update) => (
          <TimelineItem key={update.id}>
            <TimelineMarker />
            <TimelineConnector />
            <TimelineTitle>{update.author}</TimelineTitle>
            <TimelineTime dateTime={update.date}>{update.time}</TimelineTime>
            <TimelineDescription>{update.text}</TimelineDescription>
          </TimelineItem>
        ))}
      </Timeline>
      <Button
        size="sm"
        variant="outline"
        className="self-start"
        disabled={busy || count >= updates.length}
        onClick={() => {
          setBusy(true)
          timer.current = setTimeout(() => {
            setCount((current) => current + pageSize)
            setBusy(false)
          }, 600)
        }}
      >
        {busy
          ? "Loading…"
          : count >= updates.length
            ? "All caught up"
            : "Load more"}
      </Button>
    </div>
  )
}

export { UiSdkTimelineFeed }
```

### Right to left

The markers and the line move to the right side.

```tsx title="components/examples/ui-sdk-timeline/rtl.tsx"
import {
  Timeline,
  TimelineConnector,
  TimelineDescription,
  TimelineItem,
  TimelineMarker,
  TimelineTime,
  TimelineTitle,
} from "@/components/ui/ui-sdk-timeline"

const events = [
  {
    title: "تم النشر",
    dateTime: "2026-11-10T14:32",
    time: "اليوم، 2:32 م",
    description: "الإصدار 2.4.1 متاح الآن في كل المناطق.",
  },
  {
    title: "نجحت الفحوصات",
    dateTime: "2026-11-10T14:20",
    time: "اليوم، 2:20 م",
    description: "اجتاز 312 اختبارًا.",
  },
  {
    title: "تم دمج الطلب",
    dateTime: "2026-11-10T13:58",
    time: "اليوم، 1:58 م",
    description: "دمجت آدا التغييرات في الفرع الرئيسي.",
  },
]

function UiSdkTimelineRtl() {
  return (
    <div dir="rtl" lang="ar" className="w-full max-w-sm">
      <Timeline aria-label="النشاط">
        {events.map((event) => (
          <TimelineItem key={event.title}>
            <TimelineMarker />
            <TimelineConnector />
            <TimelineTitle>{event.title}</TimelineTitle>
            <TimelineTime dateTime={event.dateTime}>{event.time}</TimelineTime>
            <TimelineDescription>{event.description}</TimelineDescription>
          </TimelineItem>
        ))}
      </Timeline>
    </div>
  )
}

export { UiSdkTimelineRtl }
```

## Keyboard

| Key | Action |
| --- | --- |
| `↓` `↑` | Move between events in a vertical timeline. |
| `→` `←` | Move between events in a horizontal timeline. They swap in right-to-left text. |
| `Home` `End` | Focus the first or last event. |
| `PageDown` `PageUp` | In a feed, focus the next or previous article, from anywhere inside one. |
| `Tab` | Enters on the current event, else the first, then moves to the links and buttons inside it. |

## Accessibility

A timeline is a list named with `aria-label`, or a feed of articles with their positions. Each event is named by its title and described by its time and description, and the markers and connectors are hidden from screen readers.

The focused event shows a ring with room around it. Marker colors fade between statuses only when motion is allowed, and in forced colors the dots and lines draw in the text color.

## API reference

Each part wraps the UI SDK part of the same name and accepts all of its props, including `render` and a `className` that can be a function of the part’s state.

### Timeline

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `orientation` | `"vertical" \| "horizontal"` | `"vertical"` |  |
| `loop` | `boolean` | `true` |  |
| `feed` | `boolean` | `false` |  |
| `busy` | `boolean` | `false` | Sets aria-busy on a feed while it loads. |
| `controller` | `TimelineController` | – | From createTimelineController(). |
| `render` | `ReactElement \| (props, state) => ReactElement` | – |  |

| Attribute | Description |
| --- | --- |
| `data-slot="timeline"` | Target the timeline in CSS. |
| `data-orientation` | vertical or horizontal. |
| `data-busy` | Present while a feed is busy. |
| `--timeline-count` | How many events are in the page. |

### TimelineItem

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `string` | – | Names the event for the controller’s focus(). |
| `status` | `"complete" \| "current" \| "upcoming"` | – |  |
| `render` | `ReactElement \| (props, state) => ReactElement` | – |  |

| Attribute | Description |
| --- | --- |
| `data-slot="timeline-item"` | Target an event in CSS. |
| `data-complete` | Present when the event’s status is complete. |
| `data-current` | Present when the event’s status is current. |
| `data-upcoming` | Present when the event’s status is upcoming. |
| `data-first` | Present on the first event once it’s in the page. |
| `data-last` | Present on the last event once it’s in the page. |
| `--timeline-index` | The event’s place from 0. |

### TimelineMarker

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `variant` | `"default" \| "success" \| "warning" \| "destructive" \| "info"` | `"default"` |  |
| `children` | `ReactNode` | – | An icon or image. Empty draws a dot. |
| `render` | `ReactElement \| (props, state) => ReactElement` | – |  |

| Attribute | Description |
| --- | --- |
| `data-slot="timeline-marker"` | Target the marker in CSS. |
| `data-variant` | The marker’s variant. |
| `data-complete` | Present when the event’s status is complete. |
| `data-current` | Present when the event’s status is current. |
| `data-upcoming` | Present when the event’s status is upcoming. |

### TimelineConnector

The line from the marker to the next event. It’s solid under a complete event and hidden after the last.

### TimelineTitle

Names its event. Render it as a heading with `render={<h3 />}` so people can move between events by heading.

### TimelineTime

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `dateTime` | `string` | – | A machine-readable date, time or duration. It describes the event. |

### TimelineDescription

More about its event. It describes the event.

## Notes for AI assistants

- Install a component with the shadcn CLI: `npx shadcn@latest add https://hextaui.com/r/<name>.json`. It adds the source, the HextaUI theme tokens and any HextaUI components it depends on. `https://hextaui.com/r/all.json` installs every component.
- The code is then owned by the project, like shadcn/ui. There is no HextaUI npm package. HextaUI is MIT licensed and free for personal and commercial use.
- Behavior and accessibility come from Base UI (`@base-ui/react`). Compose with the `render` prop, not `asChild`.
- Styling uses Tailwind CSS v4 with theme tokens. Merge classes with `cn` from the `cn` package.
- Icons come from `@tabler/icons-react`.
- Import components from `@/components/ui/<name>`, hooks from `@/hooks/<name>` and utilities from `@/lib/<name>`.

Every HextaUI doc: https://hextaui.com/llms.txt

## Related

- [Avatar](https://hextaui.com/docs/avatar) - User photos with an initials fallback, status badges and stacked groups that collapse into a count.
- [Badge](https://hextaui.com/docs/badge) - Status labels with colored dots, removable tags that slide closed and counts that roll to their new value.
- [Card](https://hextaui.com/docs/card) - A surface for grouping content, with three variants, edge-to-edge media, concentric radii and whole-card links.
- [Chart](https://hextaui.com/docs/chart) - Recharts charts with themed colors, a tooltip and legend that read labels from one config, and keyboard navigation with a visible focus ring.
- [Data table](https://hextaui.com/docs/data-table) - A table for real data, with sorting, search, row selection, pinned columns, a sticky header and pagination.
- [Item](https://hextaui.com/docs/item) - A row of media, text and actions for lists, settings and pickers, with a grouped surface and a hover highlight that glides between rows.
