HextaUI

BetaGebaut auf Vercels UI SDK 0.1.0-beta.2. Die API kann sich ändern.

Timeline

Eine Timeline auf Vercels UI SDK für Aktivität, Bestellfortschritt und Agentenschritte, vertikal oder horizontal, mit Status, Zeitstempeln, Symbol-Markern und Feed-Modus.

Abhängig von
  • @ui-sdk/core
  • @ui-sdk/react
  • class-variance-authority
Deployed to production
Version 2.4.1 is live in every region.
Checks passed
312 tests and the bundle size budget.
Pull request merged
Ada merged Cache search results into main.
Review requested
Grace and Linus were asked to review.
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 }
pnpm dlx shadcn@latest add https://hextaui.com/r/ui-sdk-timeline.json

Fügt die Komponente, die HextaUI-Theme-Tokens und alle HextaUI-Komponenten hinzu, von denen sie abhängt.

UI SDK bringt etwas eigenes CSS in einem Cascade Layer namens ui-sdk mit. Deklariere diesen Layer ganz oben im globalen Stylesheet, über dem Tailwind-Import, damit deine Klassen gewinnen.

@layer ui-sdk;

@import "tailwindcss";
import {
  Timeline,
  TimelineConnector,
  TimelineDescription,
  TimelineItem,
  TimelineMarker,
  TimelineTime,
  TimelineTitle,
} from "@/components/ui/ui-sdk-timeline"
<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>

Eine Timeline listet auf, was passiert ist oder passieren wird, etwa einen Aktivitätsverlauf, den Fortschritt einer Bestellung oder die Schritte eines Agenten. Für Screenreader ist sie eine Liste mit einem Tab-Stopp, und die Pfeiltasten wechseln zwischen Ereignissen. Übergib dateTime an TimelineTime, damit das Datum maschinenlesbar ist, während der Text freundlich bleibt. UI SDK markiert sie in 0.1.0-beta.2 als experimentell.

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

Jeder Teil in einem Element ist optional. Der Marker sitzt in einer eigenen Spalte, und der Verbinder läuft von ihm zum nächsten Ereignis, nach dem letzten ausgeblendet. Alles andere in einem Element, etwa ein Button oder ein Collapsible, richtet sich am Titel aus.

Gib jedem Element einen status von complete, current oder upcoming. Das aktuelle Ereignis erhält aria-current="step" und den Tab-Stopp, und ein Controller fokussiert ein Ereignis über seinen value. Nenne den Status auch im Titel, da der Marker ihn nur zeigt.

"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 }

Ein Symbol in TimelineMarker macht ihn zu einem kleinen Kreis. Setze einen Spinner in den aktuellen Schritt, während ein Agent arbeitet, und eine Dauer wie PT5S in dateTime.

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 }

Die variant des Markers ist success, warning, destructive oder info, für einen leeren Punkt oder ein Symbol.

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 }

orientation="horizontal" legt die Ereignisse in eine Reihe, wie eine Roadmap. Links und Rechts wechseln zwischen ihnen, und loop={false} hält an den Enden.

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 }

Links und Buttons in einem Ereignis bleiben in der Tab-Reihenfolge. Tab springt vom Ereignis zu ihnen, und die Pfeiltasten wechseln weiter zwischen Ereignissen.

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 macht die Liste zu einem Feed von Artikeln für wachsende Timelines, und Bild ab und Bild auf wechseln zwischen ihnen. Setze busy, während mehr lädt.

"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 }

Marker und Linie wandern auf die rechte Seite.

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 }
TasteAktion
↓↑Wechseln zwischen Ereignissen in einer vertikalen Timeline.
→←Wechseln zwischen Ereignissen in einer horizontalen Timeline. In Rechts-nach-links-Text tauschen sie.
HomeEndFokussieren das erste oder letzte Ereignis.
PageDownPageUpIn einem Feed fokussieren sie den nächsten oder vorherigen Artikel, von überall innerhalb eines Artikels.
TabSpringt auf das aktuelle Ereignis, sonst auf das erste, dann zu den Links und Buttons darin.

Eine Timeline ist eine mit aria-label benannte Liste oder ein Feed von Artikeln mit ihren Positionen. Jedes Ereignis wird nach seinem Titel benannt und durch Zeit und Beschreibung beschrieben, und Marker und Verbinder sind für Screenreader verborgen.

Das fokussierte Ereignis zeigt einen Ring mit Abstand. Marker-Farben blenden nur bei erlaubter Bewegung zwischen Status über, und in erzwungenen Farben zeichnen Punkte und Linien in der Textfarbe.

Jeder Teil umhüllt den gleichnamigen UI-SDK-Teil und nimmt alle seine Props an, auch render und einen className, der eine Funktion des Zustands sein kann.

PropTypStandard
orientation
"vertical" | "horizontal""vertical"
loop
booleantrue
feed
booleanfalse
busySetzt aria-busy an einem Feed, solange er lädt.
booleanfalse
controllerAus createTimelineController().
TimelineController–
render
ReactElement | (props, state) => ReactElement–
AttributBeschreibung
data-slot="timeline"Zum Ansprechen der Timeline in CSS.
data-orientationvertical oder horizontal.
data-busyVorhanden, solange ein Feed beschäftigt ist.
--timeline-countWie viele Ereignisse auf der Seite sind.
PropTypStandard
valueBenennt das Ereignis für focus() des Controllers.
string–
status
"complete" | "current" | "upcoming"–
render
ReactElement | (props, state) => ReactElement–
AttributBeschreibung
data-slot="timeline-item"Zum Ansprechen eines Ereignisses in CSS.
data-completeVorhanden, wenn der Status des Ereignisses complete ist.
data-currentVorhanden, wenn der Status des Ereignisses current ist.
data-upcomingVorhanden, wenn der Status des Ereignisses upcoming ist.
data-firstAm ersten Ereignis vorhanden, sobald es auf der Seite ist.
data-lastAm letzten Ereignis vorhanden, sobald es auf der Seite ist.
--timeline-indexDie Position des Ereignisses ab 0.
PropTypStandard
variant
"default" | "success" | "warning" | "destructive" | "info""default"
childrenEin Symbol oder Bild. Leer zeichnet einen Punkt.
ReactNode–
render
ReactElement | (props, state) => ReactElement–
AttributBeschreibung
data-slot="timeline-marker"Zum Ansprechen des Markers in CSS.
data-variantDie Variante des Markers.
data-completeVorhanden, wenn der Status des Ereignisses complete ist.
data-currentVorhanden, wenn der Status des Ereignisses current ist.
data-upcomingVorhanden, wenn der Status des Ereignisses upcoming ist.

Die Linie vom Marker zum nächsten Ereignis. Unter einem abgeschlossenen Ereignis ist sie kräftig, nach dem letzten ausgeblendet.

Benennt sein Ereignis. Rendere es mit render={<h3 />} als Überschrift, damit man per Überschrift zwischen Ereignissen springen kann.

PropTypStandard
dateTimeEin maschinenlesbares Datum, eine Zeit oder Dauer. Es beschreibt das Ereignis.
string–

Mehr zu seinem Ereignis. Es beschreibt das Ereignis.