HextaUI

BêtaConstruit sur l’UI SDK 0.1.0-beta.2 de Vercel. Son API peut changer.

Timeline

Une frise chronologique sur l’UI SDK de Vercel pour l’activité, le suivi de commande et les étapes d’un agent, verticale ou horizontale, avec statuts, horodatages, icônes et mode flux.

Dépend de
  • @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

Ajoute le composant, les tokens de thème HextaUI et les composants HextaUI dont il dépend.

UI SDK ajoute un peu de CSS dans une couche de cascade nommée ui-sdk. Déclarez cette couche en haut de votre feuille de style globale, au-dessus de l’import Tailwind, pour que vos classes l’emportent.

@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>

Une frise liste ce qui s’est passé, ou se passera, comme un flux d’activité, le suivi d’une commande ou les étapes d’un agent. Pour les lecteurs d’écran, c’est une liste avec un seul arrêt de tabulation, et les flèches passent d’un événement à l’autre. Passez dateTime à TimelineTime pour que la date soit lisible par les machines tout en gardant un texte convivial. UI SDK la marque comme expérimentale en 0.1.0-beta.2.

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

Chaque partie d’un élément est facultative. Le marqueur a sa propre colonne et le connecteur va de lui à l’événement suivant, masqué après le dernier. Tout autre contenu placé dans un élément, comme un bouton ou un repliable, s’aligne sur le titre.

Donnez à chaque élément un status complete, current ou upcoming. L’événement en cours reçoit aria-current="step" et l’arrêt de tabulation, et un contrôleur cible un événement par sa value. Indiquez aussi le statut dans le titre, car le marqueur ne fait que l’afficher.

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

Une icône dans TimelineMarker le transforme en petit cercle. Placez un indicateur de chargement dans l’étape en cours pendant qu’un agent travaille, et une durée comme PT5S dans 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 }

La variant du marqueur vaut success, warning, destructive ou info, pour un point vide ou une icône.

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" aligne les événements en rangée, comme une feuille de route. Gauche et Droite passent de l’un à l’autre, et loop={false} s’arrête aux extrémités.

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 }

Les liens et boutons d’un événement restent dans l’ordre de tabulation. Tab passe de l’événement à eux, et les flèches passent toujours d’un événement à l’autre.

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 transforme la liste en flux d’articles pour les frises qui s’allongent, et Page suiv. et Page préc. passent de l’un à l’autre. Activez busy pendant le chargement.

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

Les marqueurs et la ligne passent à droite.

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 }
ToucheAction
↓↑Passent d’un événement à l’autre dans une frise verticale.
→←Passent d’un événement à l’autre dans une frise horizontale. Elles s’inversent en écriture de droite à gauche.
HomeEndCiblent le premier ou le dernier événement.
PageDownPageUpDans un flux, ciblent l’article suivant ou précédent, depuis n’importe où à l’intérieur d’un article.
TabEntre sur l’événement en cours, sinon le premier, puis passe aux liens et boutons qu’il contient.

Une frise est une liste nommée avec aria-label, ou un flux d’articles avec leurs positions. Chaque événement est nommé par son titre et décrit par son heure et sa description, et les marqueurs et connecteurs sont masqués aux lecteurs d’écran.

L’événement ciblé affiche un anneau avec de l’espace autour. Les couleurs du marqueur ne passent en fondu d’un statut à l’autre que si les animations sont autorisées, et en couleurs forcées les points et lignes prennent la couleur du texte.

Chaque partie enveloppe la partie UI SDK du même nom et accepte toutes ses props, y compris render et un className qui peut être une fonction de son état.

PropTypePar défaut
orientation
"vertical" | "horizontal""vertical"
loop
booleantrue
feed
booleanfalse
busyPose aria-busy sur un flux pendant son chargement.
booleanfalse
controllerIssu de createTimelineController().
TimelineController–
render
ReactElement | (props, state) => ReactElement–
AttributDescription
data-slot="timeline"Pour cibler la frise en CSS.
data-orientationvertical ou horizontal.
data-busyPrésent tant qu’un flux est occupé.
--timeline-countLe nombre d’événements dans la page.
PropTypePar défaut
valueNomme l’événement pour le focus() du contrôleur.
string–
status
"complete" | "current" | "upcoming"–
render
ReactElement | (props, state) => ReactElement–
AttributDescription
data-slot="timeline-item"Pour cibler un événement en CSS.
data-completePrésent quand le statut de l’événement est complete.
data-currentPrésent quand le statut de l’événement est current.
data-upcomingPrésent quand le statut de l’événement est upcoming.
data-firstPrésent sur le premier événement une fois dans la page.
data-lastPrésent sur le dernier événement une fois dans la page.
--timeline-indexLa place de l’événement à partir de 0.
PropTypePar défaut
variant
"default" | "success" | "warning" | "destructive" | "info""default"
childrenUne icône ou une image. Vide, il dessine un point.
ReactNode–
render
ReactElement | (props, state) => ReactElement–
AttributDescription
data-slot="timeline-marker"Pour cibler le marqueur en CSS.
data-variantLa variante du marqueur.
data-completePrésent quand le statut de l’événement est complete.
data-currentPrésent quand le statut de l’événement est current.
data-upcomingPrésent quand le statut de l’événement est upcoming.

La ligne du marqueur à l’événement suivant. Elle est pleine sous un événement terminé et masquée après le dernier.

Nomme son événement. Affichez-le comme titre avec render={<h3 />} pour qu’on puisse passer d’un événement à l’autre par les titres.

PropTypePar défaut
dateTimeUne date, heure ou durée lisible par les machines. Elle décrit l’événement.
string–

Plus de détails sur son événement. Il décrit l’événement.