Skip to content
  1. Activity
  2. Timeline

Timeline

Events on a single thread, so a payment, an order or a dispute can be read from start to finish.

  1. Payment created

    Payment created from the API.

  2. Checkout page opened

    Payer selected Airtel Money KE.

  3. Payment succeeded

    Funds confirmed by the provider.

import { Timeline, TimelineItem } from '@/components/timeline'
import { CheckCircleIcon, CreditCardIcon, CursorArrowRaysIcon } from '@heroicons/react/16/solid'

export default function Example() {
  return (
    <Timeline>
      <TimelineItem
        icon={CreditCardIcon}
        title="Payment created"
        time="Aug 13, 2026, 3:02 PM"
        dateTime="2026-08-13T15:02:00Z"
        description="Payment created from the API."
      />
      <TimelineItem
        icon={CursorArrowRaysIcon}
        title="Checkout page opened"
        time="Aug 13, 2026, 3:03 PM"
        dateTime="2026-08-13T15:03:00Z"
        description="Payer selected Airtel Money KE."
      />
      <TimelineItem
        icon={CheckCircleIcon}
        color="green"
        title="Payment succeeded"
        time="Aug 13, 2026, 3:05 PM"
        dateTime="2026-08-13T15:05:00Z"
        description="Funds confirmed by the provider."
      />
    </Timeline>
  )
}
<script setup lang="ts">
import Timeline from '@/components/timeline.vue'
import TimelineItem from '@/components/timeline-item.vue'
import { CheckCircleIcon, CreditCardIcon, CursorArrowRaysIcon } from '@heroicons/vue/16/solid'
</script>

<template>
  <Timeline>
    <TimelineItem
      :icon="CreditCardIcon"
      title="Payment created"
      time="Aug 13, 2026, 3:02 PM"
      date-time="2026-08-13T15:02:00Z"
      description="Payment created from the API."
    />
    <TimelineItem
      :icon="CursorArrowRaysIcon"
      title="Checkout page opened"
      time="Aug 13, 2026, 3:03 PM"
      date-time="2026-08-13T15:03:00Z"
      description="Payer selected Airtel Money KE."
    />
    <TimelineItem
      :icon="CheckCircleIcon"
      color="green"
      title="Payment succeeded"
      time="Aug 13, 2026, 3:05 PM"
      date-time="2026-08-13T15:05:00Z"
      description="Funds confirmed by the provider."
    />
  </Timeline>
</template>

Installation

Copy these files into the folder where you keep your components. They import each other with relative paths, so the folder can live anywhere. The packages they rely on are listed on the Installation page.

import clsx from 'clsx'
import type React from 'react'

const colors = {
  zinc: 'fill-zinc-400 dark:fill-zinc-500',
  red: 'fill-red-600 dark:fill-red-400',
  orange: 'fill-orange-600 dark:fill-orange-400',
  amber: 'fill-amber-600 dark:fill-amber-400',
  yellow: 'fill-yellow-600 dark:fill-yellow-400',
  lime: 'fill-lime-600 dark:fill-lime-400',
  green: 'fill-green-600 dark:fill-green-400',
  emerald: 'fill-emerald-600 dark:fill-emerald-400',
  teal: 'fill-teal-600 dark:fill-teal-400',
  cyan: 'fill-cyan-600 dark:fill-cyan-400',
  sky: 'fill-sky-600 dark:fill-sky-400',
  blue: 'fill-blue-600 dark:fill-blue-400',
  indigo: 'fill-indigo-600 dark:fill-indigo-400',
  violet: 'fill-violet-600 dark:fill-violet-400',
  purple: 'fill-purple-600 dark:fill-purple-400',
  fuchsia: 'fill-fuchsia-600 dark:fill-fuchsia-400',
  pink: 'fill-pink-600 dark:fill-pink-400',
  rose: 'fill-rose-600 dark:fill-rose-400',
}

export function Timeline({ className, ...props }: React.ComponentPropsWithoutRef<'ol'>) {
  return <ol role="list" {...props} className={className} />
}

export function TimelineItem({
  icon: Icon,
  color = 'zinc',
  marker,
  title,
  time,
  dateTime,
  description,
  action,
  className,
  children,
  ...props
}: {
  icon?: React.ComponentType<{ className?: string }>
  color?: keyof typeof colors
  marker?: React.ReactNode
  title?: React.ReactNode
  time?: React.ReactNode
  dateTime?: string
  description?: React.ReactNode
  action?: React.ReactNode
} & Omit<React.ComponentPropsWithoutRef<'li'>, 'title'>) {
  return (
    <li {...props} className={clsx(className, 'group/item relative flex gap-3 pb-6 last:pb-0')}>
      <span
        aria-hidden="true"
        className="absolute top-7 bottom-0 left-3 w-px -translate-x-1/2 bg-zinc-950/10 group-last/item:hidden dark:bg-white/10"
      />
      <span className="relative flex size-6 shrink-0 items-center justify-center rounded-full bg-white ring-1 ring-zinc-950/10 dark:bg-zinc-900 dark:ring-white/10">
        {marker ?? (Icon && <Icon className={clsx('size-4 shrink-0', colors[color])} />)}
      </span>
      <div className="min-w-0 flex-1 pt-0.5">
        {(title || time || action) && (
          <div className="flex flex-wrap items-baseline justify-between gap-x-4">
            {title && (
              <p className="min-w-0 text-base/6 font-medium text-zinc-950 sm:text-sm/6 dark:text-white">{title}</p>
            )}
            {(time || action) && (
              <div className="flex items-center gap-1.5">
                {time && (
                  <time
                    dateTime={dateTime}
                    className="text-base/6 whitespace-nowrap text-zinc-500 tabular-nums sm:text-sm/6 dark:text-zinc-400"
                  >
                    {time}
                  </time>
                )}
                {action}
              </div>
            )}
          </div>
        )}
        {description && (
          <p className="text-base/6 text-pretty text-zinc-500 sm:text-sm/6 dark:text-zinc-400">{description}</p>
        )}
        {children}
      </div>
    </li>
  )
}
<script setup lang="ts">
</script>

<template>
  <ol role="list">
    <slot />
  </ol>
</template>

Component API

PropDefaultDescription
Timelinerenders a <ol> element
This component does not expose any component-specific props.
TimelineItemrenders a <li> element
icon-A 16px Heroicon drawn in the marker.
colorzincTint of the icon. Same palette as the badge colors.
marker-Replaces the icon in the marker: a logo, an avatar.
title-First line of the event. Accepts any node, a name followed by a `Code` for example.
time-Rendered opposite the title, in a `<time>` element.
dateTime-Machine-readable value for `time`, an ISO 8601 string.
description-Line of context under the title.
action-Rendered after the time: a toggle, a menu.
PropDefaultDescription
Timelinerenders a <ol> element
This component does not expose any component-specific props.
TimelineItemrenders a <li> element
icon-A 16px Heroicon drawn in the marker.
colorzincTint of the icon. Same palette as the badge colors.
title-First line of the event. The `title` slot overrides it for rich content, a name followed by a `Code` for example.
time-Rendered opposite the title, in a `<time>` element.
dateTime-Machine-readable value for `time`, an ISO 8601 string.
description-Line of context under the title.
#marker-Slot replacing the icon in the marker: a logo, an avatar.
#action-Slot rendered after the time: a toggle, a menu.

Examples

Basic example

Each item takes an icon, a title, a time and a description. Pass the ISO value in dateTime so the time is machine-readable:

  1. Payment created

    Payment created from the API.

  2. Checkout page opened

    Payer selected Airtel Money KE.

  3. Payment succeeded

    Funds confirmed by the provider.

import { Timeline, TimelineItem } from '@/components/timeline'
import { CheckCircleIcon, CreditCardIcon, CursorArrowRaysIcon } from '@heroicons/react/16/solid'

export default function Example() {
  return (
    <Timeline>
      <TimelineItem
        icon={CreditCardIcon}
        title="Payment created"
        time="Aug 13, 2026, 3:02 PM"
        dateTime="2026-08-13T15:02:00Z"
        description="Payment created from the API."
      />
      <TimelineItem
        icon={CursorArrowRaysIcon}
        title="Checkout page opened"
        time="Aug 13, 2026, 3:03 PM"
        dateTime="2026-08-13T15:03:00Z"
        description="Payer selected Airtel Money KE."
      />
      <TimelineItem
        icon={CheckCircleIcon}
        color="green"
        title="Payment succeeded"
        time="Aug 13, 2026, 3:05 PM"
        dateTime="2026-08-13T15:05:00Z"
        description="Funds confirmed by the provider."
      />
    </Timeline>
  )
}
<script setup lang="ts">
import Timeline from '@/components/timeline.vue'
import TimelineItem from '@/components/timeline-item.vue'
import { CheckCircleIcon, CreditCardIcon, CursorArrowRaysIcon } from '@heroicons/vue/16/solid'
</script>

<template>
  <Timeline>
    <TimelineItem
      :icon="CreditCardIcon"
      title="Payment created"
      time="Aug 13, 2026, 3:02 PM"
      date-time="2026-08-13T15:02:00Z"
      description="Payment created from the API."
    />
    <TimelineItem
      :icon="CursorArrowRaysIcon"
      title="Checkout page opened"
      time="Aug 13, 2026, 3:03 PM"
      date-time="2026-08-13T15:03:00Z"
      description="Payer selected Airtel Money KE."
    />
    <TimelineItem
      :icon="CheckCircleIcon"
      color="green"
      title="Payment succeeded"
      time="Aug 13, 2026, 3:05 PM"
      date-time="2026-08-13T15:05:00Z"
      description="Funds confirmed by the provider."
    />
  </Timeline>
</template>

Colors

Use color to tint the icon. Keep it for the outcome of an event, settled, failed or pending, and leave the rest neutral so the tinted ones stand out:

  1. Refund requested

    Full amount, requested from the dashboard.

  2. Refund failed

    No confirmation received from the operator within 90s.

  3. Refund issued

    Funds returned to the payer wallet.

<Timeline>
  <TimelineItem icon={ArrowUturnLeftIcon} color="amber" title="Refund requested" time="Sep 2, 2026, 9:14 AM" description="Full amount, requested from the dashboard." />
  <TimelineItem icon={XCircleIcon} color="red" title="Refund failed" time="Sep 2, 2026, 9:15 AM" description="No confirmation received from the operator within 90s." />
  <TimelineItem icon={CheckCircleIcon} color="green" title="Refund issued" time="Sep 2, 2026, 9:41 AM" description="Funds returned to the payer wallet." />
</Timeline>
<template>
  <Timeline>
    <TimelineItem :icon="ArrowUturnLeftIcon" color="amber" title="Refund requested" time="Sep 2, 2026, 9:14 AM" description="Full amount, requested from the dashboard." />
    <TimelineItem :icon="XCircleIcon" color="red" title="Refund failed" time="Sep 2, 2026, 9:15 AM" description="No confirmation received from the operator within 90s." />
    <TimelineItem :icon="CheckCircleIcon" color="green" title="Refund issued" time="Sep 2, 2026, 9:41 AM" description="Funds returned to the payer wallet." />
  </Timeline>
</template>

Custom markers

Pass a marker to replace the icon with anything sized to the ring, an avatar for the person behind the event, a logo for a provider:

  1. Dispute opened

    Chargeback from the issuing bank, evidence due in 7 days.

  2. Kofi Mensah assigned the dispute to you

  3. Ines Carvalho commented

    Evidence uploaded, the payer confirmed the delivery address.

import { Avatar } from '@/components/avatar'
import { Timeline, TimelineItem } from '@/components/timeline'
import { ScaleIcon } from '@heroicons/react/16/solid'

export default function Example() {
  return (
    <Timeline>
      <TimelineItem
        icon={ScaleIcon}
        title="Dispute opened"
        time="Sep 1, 2026"
        description="Chargeback from the issuing bank, evidence due in 7 days."
      />
      <TimelineItem
        marker={<Avatar initials="NB" className="size-6 bg-zinc-900 text-white dark:bg-white dark:text-black" />}
        title="Kofi Mensah assigned the dispute to you"
        time="Yesterday"
      />
      <TimelineItem
        marker={<Avatar src="/users/avatar.jpg" className="size-6" />}
        title="Ines Carvalho commented"
        time="2h ago"
        description="Evidence uploaded, the payer confirmed the delivery address."
      />
    </Timeline>
  )
}
<script setup lang="ts">
import Avatar from '@/components/avatar.vue'
import Timeline from '@/components/timeline.vue'
import TimelineItem from '@/components/timeline-item.vue'
import { ScaleIcon } from '@heroicons/vue/16/solid'
</script>

<template>
  <Timeline>
    <TimelineItem
      :icon="ScaleIcon"
      title="Dispute opened"
      time="Sep 1, 2026"
      description="Chargeback from the issuing bank, evidence due in 7 days."
    />
    <TimelineItem title="Kofi Mensah assigned the dispute to you" time="Yesterday">
      <template #marker>
        <Avatar initials="NB" class="size-6 bg-zinc-900 text-white dark:bg-white dark:text-black" />
      </template>
    </TimelineItem>
    <TimelineItem
      title="Ines Carvalho commented"
      time="2h ago"
      description="Evidence uploaded, the payer confirmed the delivery address."
    >
      <template #marker>
        <Avatar src="/users/avatar.jpg" class="size-6" />
      </template>
    </TimelineItem>
  </Timeline>
</template>

Without icons

Leave out the icon and the marker stays a plain ring, for a list where every entry is of the same kind, a delivery status or a changelog:

  1. Order placed

    Paid with Airtel Money.

  2. Packed

    Two parcels, 3.2 kg.

  3. Handed to the carrier

  4. Delivered

    Signed by the recipient.

<Timeline>
  <TimelineItem title="Order placed" time="Aug 13" description="Paid with Airtel Money." />
  <TimelineItem title="Packed" time="Aug 14" description="Two parcels, 3.2 kg." />
  <TimelineItem title="Handed to the carrier" time="Aug 14" />
  <TimelineItem title="Delivered" time="Aug 16" description="Signed by the recipient." />
</Timeline>
<template>
  <Timeline>
    <TimelineItem title="Order placed" time="Aug 13" description="Paid with Airtel Money." />
    <TimelineItem title="Packed" time="Aug 14" description="Two parcels, 3.2 kg." />
    <TimelineItem title="Handed to the carrier" time="Aug 14" />
    <TimelineItem title="Delivered" time="Aug 16" description="Signed by the recipient." />
  </Timeline>
</template>

Expandable details

The action slot sits after the time. Put a toggle there and render the details as children: badges, the account used, then a grid of facts that collapses to two columns on small screens:

The action slot sits after the time. Put a toggle there and render the details in the default slot: badges, the account used, then a grid of facts that collapses to two columns on small screens:

  1. Payment created

    Payment created from the API.

  2. Checkout page opened

    Payer selected Airtel Money KE.

  3. Airtel Money ke.airtel

    Succeeded

    +254 •• •• •• 25 Mobile money

    Duration
    1.5s
    External ID
    EXT-49414L4SATP
    IP address
    41.75.75.75
    Reference
    trx_W49414L4SATP
    Risk score
    3/100 · Low risk
  4. Payment succeeded

    Funds confirmed by the provider.

'use client'

import { Avatar } from '@/components/avatar'
import { Badge } from '@/components/badge'
import { TouchTarget } from '@/components/button'
import { Code } from '@/components/text'
import { TimelineItem } from '@/components/timeline'
import { ChevronDownIcon } from '@heroicons/react/16/solid'
import clsx from 'clsx'
import { useId, useState } from 'react'

export function ProviderCall() {
  let [open, setOpen] = useState(true)
  let panelId = useId()

  return (
    <TimelineItem
      marker={<Avatar initials="AM" className="size-6 bg-red-600 text-white" />}
      title={<>Airtel Money <Code>ke.airtel</Code></>}
      time="Aug 13, 2026, 3:04 PM"
      dateTime="2026-08-13T15:04:00Z"
      action={
        <button
          type="button"
          aria-expanded={open}
          aria-controls={panelId}
          aria-label="Toggle details"
          onClick={() => setOpen(!open)}
          className="relative -my-1 cursor-pointer rounded-md p-1 hover:bg-zinc-950/5 focus:outline-hidden focus-visible:outline-2 focus-visible:outline-blue-500 dark:hover:bg-white/10"
        >
          <TouchTarget>
            <ChevronDownIcon className={clsx('size-4 fill-zinc-400 transition-transform', open && 'rotate-180')} />
          </TouchTarget>
        </button>
      }
    >
      <div className="mt-1 flex flex-wrap items-center gap-2">
        <Badge color="green">Succeeded</Badge>
      </div>
      <dl id={panelId} hidden={!open} className="mt-3 grid grid-cols-2 gap-x-6 gap-y-3 sm:grid-cols-4">
        <div>
          <dt className="text-sm/6 text-zinc-500 sm:text-xs/6">Duration</dt>
          <dd className="text-base/6 text-zinc-950 tabular-nums sm:text-sm/6">1.5s</dd>
        </div>
        {/* … */}
      </dl>
    </TimelineItem>
  )
}
<script setup lang="ts">
import Avatar from '@/components/avatar.vue'
import Badge from '@/components/badge.vue'
import Code from '@/components/code.vue'
import TimelineItem from '@/components/timeline-item.vue'
import TouchTarget from '@/components/touch-target.vue'
import { ChevronDownIcon } from '@heroicons/vue/16/solid'
import { ref, useId } from 'vue'

const open = ref(true)
const panelId = useId()
</script>

<template>
  <TimelineItem time="Aug 13, 2026, 3:04 PM" date-time="2026-08-13T15:04:00Z">
    <template #marker>
      <Avatar initials="AM" class="size-6 bg-red-600 text-white" />
    </template>
    <template #title>Airtel Money <Code>ke.airtel</Code></template>
    <template #action>
      <button
        type="button"
        :aria-expanded="open"
        :aria-controls="panelId"
        aria-label="Toggle details"
        class="relative -my-1 cursor-pointer rounded-md p-1 hover:bg-zinc-950/5 focus:outline-hidden focus-visible:outline-2 focus-visible:outline-blue-500 dark:hover:bg-white/10"
        @click="open = !open"
      >
        <TouchTarget>
          <ChevronDownIcon class="size-4 fill-zinc-400 transition-transform" :class="open && 'rotate-180'" />
        </TouchTarget>
      </button>
    </template>

    <div class="mt-1 flex flex-wrap items-center gap-2">
      <Badge color="green">Succeeded</Badge>
    </div>
    <dl v-show="open" :id="panelId" class="mt-3 grid grid-cols-2 gap-x-6 gap-y-3 sm:grid-cols-4">
      <div>
        <dt class="text-sm/6 text-zinc-500 sm:text-xs/6">Duration</dt>
        <dd class="text-base/6 text-zinc-950 tabular-nums sm:text-sm/6">1.5s</dd>
      </div>
    </dl>
  </TimelineItem>
</template>