Timeline

An ordered sequence of events with connected markers.

  1. Item A
    A description for the first item.
  2. Item B
    This longer description wraps onto multiple lines. The connector extends with the content, keeping the next event below it.
  3. Item C
    The connector ends at the last item.
<script setup lang="ts">
  import { Timeline } from '@hina-ui/vue'

  const items = [
    {
      id: 'a',
      title: 'Item A',
      description: 'A description for the first item.',
      time: '09:00',
      dateTime: '2026-09-16T09:00:00+08:00',
    },
    {
      id: 'b',
      title: 'Item B',
      description:
        'This longer description wraps onto multiple lines. The connector extends with the content, keeping the next event below it.',
      time: '09:20',
      dateTime: '2026-09-16T09:20:00+08:00',
    },
    {
      id: 'c',
      title: 'Item C',
      description: 'The connector ends at the last item.',
      time: '09:40',
      dateTime: '2026-09-16T09:40:00+08:00',
    },
  ]
</script>

<template>
  <Timeline :items="items" class="max-w-lg" />
</template>
vue

Usage

import { Timeline, type TimelineItem } from '@hina-ui/vue'
ts

items defines the event order. Each item can provide a title, description and time. Connectors extend with content height and end at the last marker. Provide stable id values when items update or reorder.

  1. Item A
    A description for the first item.
  2. Item B
    This longer description wraps onto multiple lines. The connector extends with the content, keeping the next event below it.
  3. Item C
    The connector ends at the last item.
<script setup lang="ts">
  import { Timeline } from '@hina-ui/vue'

  const items = [
    {
      id: 'a',
      title: 'Item A',
      description: 'A description for the first item.',
      time: '09:00',
      dateTime: '2026-09-16T09:00:00+08:00',
    },
    {
      id: 'b',
      title: 'Item B',
      description:
        'This longer description wraps onto multiple lines. The connector extends with the content, keeping the next event below it.',
      time: '09:20',
      dateTime: '2026-09-16T09:20:00+08:00',
    },
    {
      id: 'c',
      title: 'Item C',
      description: 'The connector ends at the last item.',
      time: '09:40',
      dateTime: '2026-09-16T09:40:00+08:00',
    },
  ]
</script>

<template>
  <Timeline :items="items" />
</template>
vue

Examples

Marker colors

tone sets the default marker color. An item’s tone overrides it. Colors do not imply completion or selection; express status in text as well.

  1. accent
  2. neutral
  3. success
  4. warning
  5. danger
  6. info
<script setup lang="ts">
  import { Timeline, type TimelineTone } from '@hina-ui/vue'

  const tones: TimelineTone[] = ['accent', 'neutral', 'success', 'warning', 'danger', 'info']
  const items = tones.map(tone => ({ id: tone, title: tone, tone }))
</script>

<template>
  <Timeline :items="items" />
</template>
vue

Alignment

align="start" places the axis at the start with content after it. end places the axis at the end with content before it. alternate centers the axis and alternates the content between both sides. Both sides have equal width in the vertical alternate layout.

  1. Item A
    A description for the first item.
  2. Item B
    This longer description wraps onto multiple lines. The connector extends with the content, keeping the next event below it.
  3. Item C
    The connector ends at the last item.
<script setup lang="ts">
  import { ref } from 'vue'
  import { Timeline, Select, Stack, type TimelineAlign } from '@hina-ui/vue'

  const align = ref<TimelineAlign>('alternate')
  const options = [
    { value: 'start', label: 'start' },
    { value: 'end', label: 'end' },
    { value: 'alternate', label: 'alternate' },
  ]
  const items = [
    {
      id: 'a',
      title: 'Item A',
      description: 'A description for the first item.',
      time: '09:00',
      dateTime: '2026-09-16T09:00:00+08:00',
    },
    {
      id: 'b',
      title: 'Item B',
      description:
        'This longer description wraps onto multiple lines. The connector extends with the content, keeping the next event below it.',
      time: '09:20',
      dateTime: '2026-09-16T09:20:00+08:00',
    },
    {
      id: 'c',
      title: 'Item C',
      description: 'The connector ends at the last item.',
      time: '09:40',
      dateTime: '2026-09-16T09:40:00+08:00',
    },
  ]
</script>

<template>
  <Stack class="w-full" gap="lg">
    <Select v-model="align" :options="options" aria-label="Alignment" class="w-40" />
    <Timeline :items="items" :align="align" />
  </Stack>
</template>
vue

Opposite time

time-position="opposite" moves time labels to the other side of the axis, with equal-width sides in the vertical layout. #opposite replaces that region; providing it reserves space on the opposite side.

time is displayed as supplied. dateTime adds a machine-readable value and makes the default time label a time element. For localized formatting, place Time in #time.

  1. Item A
    A description for the first item.
  2. Item B
    This longer description wraps onto multiple lines. The connector extends with the content, keeping the next event below it.
  3. Item C
    The connector ends at the last item.
<script setup lang="ts">
  import { Timeline } from '@hina-ui/vue'

  const items = [
    {
      id: 'a',
      title: 'Item A',
      description: 'A description for the first item.',
      time: '09:00',
      dateTime: '2026-09-16T09:00:00+08:00',
    },
    {
      id: 'b',
      title: 'Item B',
      description:
        'This longer description wraps onto multiple lines. The connector extends with the content, keeping the next event below it.',
      time: '09:20',
      dateTime: '2026-09-16T09:20:00+08:00',
    },
    {
      id: 'c',
      title: 'Item C',
      description: 'The connector ends at the last item.',
      time: '09:40',
      dateTime: '2026-09-16T09:40:00+08:00',
    },
  ]
</script>

<template>
  <Timeline :items="items" time-position="opposite" />
</template>
vue

Horizontal

orientation="horizontal" arranges markers horizontally. start places content below the axis, end above it, and alternate alternates above and below. Content of different lengths shares the same horizontal axis.

Items divide the available width equally. For more items, set a minimum width and wrap the timeline in ScrollArea. The component does not automatically change direction or clip content.

  1. Item A
    A description for the first item.
  2. Item B
    This longer description wraps onto multiple lines. The connector extends with the content, keeping the next event below it.
  3. Item C
    The connector ends at the last item.
<script setup lang="ts">
  import { ref } from 'vue'
  import { Timeline, Select, Stack, ScrollArea, type TimelineAlign } from '@hina-ui/vue'

  const align = ref<TimelineAlign>('start')
  const options = [
    { value: 'start', label: 'start' },
    { value: 'end', label: 'end' },
    { value: 'alternate', label: 'alternate' },
  ]
  const items = [
    {
      id: 'a',
      title: 'Item A',
      description: 'A description for the first item.',
      time: '09:00',
      dateTime: '2026-09-16T09:00:00+08:00',
    },
    {
      id: 'b',
      title: 'Item B',
      description:
        'This longer description wraps onto multiple lines. The connector extends with the content, keeping the next event below it.',
      time: '09:20',
      dateTime: '2026-09-16T09:20:00+08:00',
    },
    {
      id: 'c',
      title: 'Item C',
      description: 'The connector ends at the last item.',
      time: '09:40',
      dateTime: '2026-09-16T09:40:00+08:00',
    },
  ]
</script>

<template>
  <Stack class="w-full" gap="lg">
    <Select v-model="align" :options="options" aria-label="Alignment" class="w-40" />
    <ScrollArea direction="horizontal" :shadow="false" class="w-full">
      <Timeline :items="items" orientation="horizontal" :align="align" class="min-w-lg" />
    </ScrollArea>
  </Stack>
</template>
vue

Sizes

size controls typography and spacing between events. Spacing follows density tokens; md is the default.

sm

  1. Item A
    A description for the first item.
  2. Item B
    This longer description wraps onto multiple lines. The connector extends with the content, keeping the next event below it.
  3. Item C
    The connector ends at the last item.

md

  1. Item A
    A description for the first item.
  2. Item B
    This longer description wraps onto multiple lines. The connector extends with the content, keeping the next event below it.
  3. Item C
    The connector ends at the last item.

lg

  1. Item A
    A description for the first item.
  2. Item B
    This longer description wraps onto multiple lines. The connector extends with the content, keeping the next event below it.
  3. Item C
    The connector ends at the last item.
<script setup lang="ts">
  import { SimpleGrid, Stack, Text, Timeline, type TimelineSize } from '@hina-ui/vue'

  const sizes: TimelineSize[] = ['sm', 'md', 'lg']
  const items = [
    {
      id: 'a',
      title: 'Item A',
      description: 'A description for the first item.',
      time: '09:00',
      dateTime: '2026-09-16T09:00:00+08:00',
    },
    {
      id: 'b',
      title: 'Item B',
      description:
        'This longer description wraps onto multiple lines. The connector extends with the content, keeping the next event below it.',
      time: '09:20',
      dateTime: '2026-09-16T09:20:00+08:00',
    },
    {
      id: 'c',
      title: 'Item C',
      description: 'The connector ends at the last item.',
      time: '09:40',
      dateTime: '2026-09-16T09:40:00+08:00',
    },
  ]
</script>

<template>
  <SimpleGrid min="12rem" class="w-full" gap="lg">
    <Stack v-for="size in sizes" :key="size">
      <Text size="sm" tone="muted">{{ size }}</Text>
      <Timeline :items="items" :size="size" />
    </Stack>
  </SimpleGrid>
</template>
vue

Custom markers and content

#marker replaces the node with Avatar, Spinner or an icon. #content replaces the entire content region, including the default time, title and description. Use #time, #title or #description to replace an individual part.

Every slot receives { item, index }. item retains custom field types and index is its position in the displayed order. This example composes Card and Tag in the content and shows a spinner on a pending item.

  1. Completed

    Done

    Compose other components in the content and use an avatar as the marker.

    Hina

  2. Updated

    Done

    Slots retain access to custom fields on the item.

    Hina

  3. In progress

    Pending

    A pending item can use a loading indicator.

    Hina

<script setup lang="ts">
  import { Check } from '@lucide/vue'
  import {
    Avatar,
    Card,
    Inline,
    Spinner,
    Stack,
    Tag,
    Text,
    Time,
    Timeline,
    type TimelineItem,
  } from '@hina-ui/vue'

  interface Item extends TimelineItem {
    author: string
    pending: boolean
  }

  const items: Item[] = [
    {
      id: 'a',
      title: 'Completed',
      description: 'Compose other components in the content and use an avatar as the marker.',
      time: '09:00',
      dateTime: '2026-09-16T09:00:00+08:00',
      author: 'Hina',
      pending: false,
      tone: 'success',
    },
    {
      id: 'b',
      title: 'Updated',
      description: 'Slots retain access to custom fields on the item.',
      time: '09:20',
      dateTime: '2026-09-16T09:20:00+08:00',
      author: 'Hina',
      pending: false,
      tone: 'success',
    },
    {
      id: 'c',
      title: 'In progress',
      description: 'A pending item can use a loading indicator.',
      time: '09:40',
      dateTime: '2026-09-16T09:40:00+08:00',
      author: 'Hina',
      pending: true,
      tone: 'accent',
    },
  ]
</script>

<template>
  <Timeline :items="items" class="max-w-xl">
    <template #marker="{ item, index }">
      <Avatar v-if="index === 0" :name="item.author" size="sm" />
      <Spinner v-else-if="item.pending" size="sm" />
      <Check v-else />
    </template>
    <template #content="{ item }">
      <Card>
        <Stack gap="sm">
          <Inline justify="between">
            <Text weight="medium">{{ item.title }}</Text>
            <Tag :tone="item.pending ? 'accent' : 'success'">
              {{ item.pending ? 'Pending' : 'Done' }}
            </Tag>
          </Inline>
          <Text size="sm" tone="muted">{{ item.description }}</Text>
          <Inline gap="sm">
            <Text size="xs" tone="muted">{{ item.author }}</Text>
            <Time :value="item.dateTime" format="time" class="text-muted text-xs" />
          </Inline>
        </Stack>
      </Card>
    </template>
  </Timeline>
</template>
vue

Reverse order

reverse reverses the rendered order without mutating the input array. The final connector, slot indexes and alternating positions follow the displayed order.

  1. Item A
    A description for the first item.
  2. Item B
    This longer description wraps onto multiple lines. The connector extends with the content, keeping the next event below it.
  3. Item C
    The connector ends at the last item.
<script setup lang="ts">
  import { ref } from 'vue'
  import { Stack, Switch, Timeline } from '@hina-ui/vue'

  const reverse = ref(false)
  const items = [
    {
      id: 'a',
      title: 'Item A',
      description: 'A description for the first item.',
      time: '09:00',
      dateTime: '2026-09-16T09:00:00+08:00',
    },
    {
      id: 'b',
      title: 'Item B',
      description:
        'This longer description wraps onto multiple lines. The connector extends with the content, keeping the next event below it.',
      time: '09:20',
      dateTime: '2026-09-16T09:20:00+08:00',
    },
    {
      id: 'c',
      title: 'Item C',
      description: 'The connector ends at the last item.',
      time: '09:40',
      dateTime: '2026-09-16T09:40:00+08:00',
    },
  ]
</script>

<template>
  <Stack class="w-full" gap="lg">
    <Switch v-model="reverse">Reverse order</Switch>
    <Timeline :items="items" :reverse="reverse" />
  </Stack>
</template>
vue

RTL

Direction is inherited from an ancestor’s dir, or set directly with dir="rtl". Vertical start/end positions and horizontal item order follow it.

  1. 09:00
    البند الأول
    وصف موجز للبند الأول.
  2. 09:20
    البند الثاني
    وصف البند الثاني.
  3. 09:40
    البند الثالث
<script setup lang="ts">
  import { Timeline } from '@hina-ui/vue'

  const items = [
    { id: 'a', title: 'البند الأول', description: 'وصف موجز للبند الأول.', time: '09:00' },
    { id: 'b', title: 'البند الثاني', description: 'وصف البند الثاني.', time: '09:20' },
    { id: 'c', title: 'البند الثالث', time: '09:40' },
  ]
</script>

<template>
  <Timeline :items="items" dir="rtl" />
</template>
vue

Accessibility

  • The root is an ordered list. Items are read in DOM order; use aria-label to name the list.
  • Markers and connectors are decorative and hidden from assistive technology. Keep marker slots non-interactive; put links and buttons in content or opposite slots for normal keyboard access.
  • No extra tab stops or implicit selected, completed or current-step semantics are added.
  • There is no built-in animation. Custom loading indicators follow their own reduced-motion settings.

API

Props

Prop
Type
Default
Description
items
T[]
Required
Ordered items, where T extends TimelineItem
orientation
'vertical' | 'horizontal'
'vertical'
Layout direction
align
'start' | 'end' | 'alternate'
'start'
Position of the axis relative to content
size
'sm' | 'md' | 'lg'
'md'
Typography and event spacing
tone
TimelineTone
'accent'
Default marker color
timePosition
'content' | 'opposite'
'content'
Default time label position
reverse
boolean
false
Reverse the displayed order
class
string
—
Root element classes

Native attributes such as dir, aria-label and style are forwarded to the root.

TimelineItem

Field
Type
Description
id
string | number
Stable identity; falls back to the displayed index
title
string
Title
description
string
Description
time
string
Time text displayed without formatting
dateTime
string
The time element’s datetime attribute; does not format or sort items
tone
TimelineTone
Marker color override

TimelineTone: 'accent' | 'neutral' | 'success' | 'warning' | 'danger' | 'info'. All item fields are optional; slots can supply the full presentation.

Slots

Slot
Props
Description
marker
{ item: T, index: number }
Decorative marker; a hollow dot by default
content
{ item: T, index: number }
Entire content region
title
{ item: T, index: number }
Title
description
{ item: T, index: number }
Description
time
{ item: T, index: number }
Time label, positioned by timePosition
opposite
{ item: T, index: number }
Opposite region, overriding the opposite time label

Also exports TimelineItem, TimelineSlotProps<T>, TimelineTone, TimelineSize, TimelineOrientation and TimelineAlign types.