- Item AA description for the first item.
- Item BThis longer description wraps onto multiple lines. The connector extends with the content, keeping the next event below it.
- Item CThe 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>
Usage
import { Timeline, type TimelineItem } from '@hina-ui/vue'
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.
- Item AA description for the first item.
- Item BThis longer description wraps onto multiple lines. The connector extends with the content, keeping the next event below it.
- Item CThe 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>
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.
- accent
- neutral
- success
- warning
- danger
- 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>
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.
- Item AA description for the first item.
- Item BThis longer description wraps onto multiple lines. The connector extends with the content, keeping the next event below it.
- Item CThe 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>
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.
- Item AA description for the first item.
- Item BThis longer description wraps onto multiple lines. The connector extends with the content, keeping the next event below it.
- Item CThe 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>
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.
- Item AA description for the first item.
- Item BThis longer description wraps onto multiple lines. The connector extends with the content, keeping the next event below it.
- Item CThe 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>
Sizes
size controls typography and spacing between events. Spacing follows density tokens; md is the default.
sm
- Item AA description for the first item.
- Item BThis longer description wraps onto multiple lines. The connector extends with the content, keeping the next event below it.
- Item CThe connector ends at the last item.
md
- Item AA description for the first item.
- Item BThis longer description wraps onto multiple lines. The connector extends with the content, keeping the next event below it.
- Item CThe connector ends at the last item.
lg
- Item AA description for the first item.
- Item BThis longer description wraps onto multiple lines. The connector extends with the content, keeping the next event below it.
- Item CThe 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>
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.
Completed
DoneCompose other components in the content and use an avatar as the marker.
Hina
Updated
DoneSlots retain access to custom fields on the item.
Hina
In progress
PendingA 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>
Reverse order
reverse reverses the rendered order without mutating the input array. The final connector, slot indexes and alternating positions follow the displayed order.
- Item AA description for the first item.
- Item BThis longer description wraps onto multiple lines. The connector extends with the content, keeping the next event below it.
- Item CThe 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>
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.
- 09:00البند الأولوصف موجز للبند الأول.
- 09:20البند الثانيوصف البند الثاني.
- 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>
Accessibility
- The root is an ordered list. Items are read in DOM order; use
aria-labelto 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.