- 条目 A第一项的说明文字。
- 条目 B这一项包含更长的说明文字,换行后节点之间的连线仍然连续,后面的内容随之向下排列。
- 条目 C最后一项不继续绘制连线。
import { Timeline } from '@hina-ui/react'
const items = [
{
id: 'a',
title: '条目 A',
description: '第一项的说明文字。',
time: '09:00',
dateTime: '2026-09-16T09:00:00+08:00',
},
{
id: 'b',
title: '条目 B',
description: '这一项包含更长的说明文字,换行后节点之间的连线仍然连续,后面的内容随之向下排列。',
time: '09:20',
dateTime: '2026-09-16T09:20:00+08:00',
},
{
id: 'c',
title: '条目 C',
description: '最后一项不继续绘制连线。',
time: '09:40',
dateTime: '2026-09-16T09:40:00+08:00',
},
]
export default function Demo() {
return <Timeline items={items} className="max-w-lg" />
}
用法
import { Timeline, type TimelineItem } from '@hina-ui/react'
items 定义节点顺序,每项可包含 title、description 和 time。连线随内容高度延伸,最后一项没有尾线。条目会更新或重排时,提供稳定的 id。
- 条目 A第一项的说明文字。
- 条目 B这一项包含更长的说明文字,换行后节点之间的连线仍然连续,后面的内容随之向下排列。
- 条目 C最后一项不继续绘制连线。
import { Timeline } from '@hina-ui/react'
const items = [
{
id: 'a',
title: '条目 A',
description: '第一项的说明文字。',
time: '09:00',
dateTime: '2026-09-16T09:00:00+08:00',
},
{
id: 'b',
title: '条目 B',
description: '这一项包含更长的说明文字,换行后节点之间的连线仍然连续,后面的内容随之向下排列。',
time: '09:20',
dateTime: '2026-09-16T09:20:00+08:00',
},
{
id: 'c',
title: '条目 C',
description: '最后一项不继续绘制连线。',
time: '09:40',
dateTime: '2026-09-16T09:40:00+08:00',
},
]
export default function Demo() {
return <Timeline items={items} />
}
示例
节点颜色
tone 设置默认节点颜色,单项的 tone 可覆盖它。颜色不表示隐含的完成或选中状态,状态信息需要同时用文字表达。
- accent
- neutral
- success
- warning
- danger
- info
import { Timeline, type TimelineTone } from '@hina-ui/react'
const tones: TimelineTone[] = ['accent', 'neutral', 'success', 'warning', 'danger', 'info']
const items = tones.map(tone => ({ id: tone, title: tone, tone }))
export default function Demo() {
return <Timeline items={items} />
}
对齐方式
align="start" 将节点轴放在起始侧,内容排在轴后;end 将轴放在结束侧,内容排在轴前;alternate 将轴居中,内容交替排在两侧。纵向交替排布时,两侧等宽。
- 条目 A第一项的说明文字。
- 条目 B这一项包含更长的说明文字,换行后节点之间的连线仍然连续,后面的内容随之向下排列。
- 条目 C最后一项不继续绘制连线。
'use client'
import { useState } from 'react'
import { Timeline, Select, Stack, type TimelineAlign } from '@hina-ui/react'
const options = [
{ value: 'start', label: 'start' },
{ value: 'end', label: 'end' },
{ value: 'alternate', label: 'alternate' },
]
const items = [
{
id: 'a',
title: '条目 A',
description: '第一项的说明文字。',
time: '09:00',
dateTime: '2026-09-16T09:00:00+08:00',
},
{
id: 'b',
title: '条目 B',
description: '这一项包含更长的说明文字,换行后节点之间的连线仍然连续,后面的内容随之向下排列。',
time: '09:20',
dateTime: '2026-09-16T09:20:00+08:00',
},
{
id: 'c',
title: '条目 C',
description: '最后一项不继续绘制连线。',
time: '09:40',
dateTime: '2026-09-16T09:40:00+08:00',
},
]
export default function Demo() {
const [align, setAlign] = useState<TimelineAlign>('alternate')
return (
<Stack className="w-full" gap="lg">
<Select
value={align}
onValueChange={value => setAlign(value as TimelineAlign)}
options={options}
aria-label="对齐方式"
className="w-40"
/>
<Timeline items={items} align={align} />
</Stack>
)
}
对侧时间
timePosition="opposite" 将时间移到轴的另一侧,纵向两侧等宽。renderOpposite 可替换对侧区域;提供它时会预留对侧空间。
time 是直接显示的文字,dateTime 为其补充机器可读的值。提供 dateTime 时默认时间渲染为 time 元素;需要本地化格式时,可通过 renderTime 放入 Time。
- 条目 A第一项的说明文字。
- 条目 B这一项包含更长的说明文字,换行后节点之间的连线仍然连续,后面的内容随之向下排列。
- 条目 C最后一项不继续绘制连线。
import { Timeline } from '@hina-ui/react'
const items = [
{
id: 'a',
title: '条目 A',
description: '第一项的说明文字。',
time: '09:00',
dateTime: '2026-09-16T09:00:00+08:00',
},
{
id: 'b',
title: '条目 B',
description: '这一项包含更长的说明文字,换行后节点之间的连线仍然连续,后面的内容随之向下排列。',
time: '09:20',
dateTime: '2026-09-16T09:20:00+08:00',
},
{
id: 'c',
title: '条目 C',
description: '最后一项不继续绘制连线。',
time: '09:40',
dateTime: '2026-09-16T09:40:00+08:00',
},
]
export default function Demo() {
return <Timeline items={items} timePosition="opposite" />
}
横向
orientation="horizontal" 将节点水平排列。start 的正文位于轴下,end 位于轴上,alternate 上下交替。不同长度的内容仍共用同一条水平轴。
横向条目等分可用宽度;条目较多时,可以设置最小宽度并放入 ScrollArea。组件不会自动改变方向或裁切内容。
- 条目 A第一项的说明文字。
- 条目 B这一项包含更长的说明文字,换行后节点之间的连线仍然连续,后面的内容随之向下排列。
- 条目 C最后一项不继续绘制连线。
'use client'
import { useState } from 'react'
import { Timeline, Select, Stack, ScrollArea, type TimelineAlign } from '@hina-ui/react'
const options = [
{ value: 'start', label: 'start' },
{ value: 'end', label: 'end' },
{ value: 'alternate', label: 'alternate' },
]
const items = [
{
id: 'a',
title: '条目 A',
description: '第一项的说明文字。',
time: '09:00',
dateTime: '2026-09-16T09:00:00+08:00',
},
{
id: 'b',
title: '条目 B',
description: '这一项包含更长的说明文字,换行后节点之间的连线仍然连续,后面的内容随之向下排列。',
time: '09:20',
dateTime: '2026-09-16T09:20:00+08:00',
},
{
id: 'c',
title: '条目 C',
description: '最后一项不继续绘制连线。',
time: '09:40',
dateTime: '2026-09-16T09:40:00+08:00',
},
]
export default function Demo() {
const [align, setAlign] = useState<TimelineAlign>('start')
return (
<Stack className="w-full" gap="lg">
<Select
value={align}
onValueChange={value => setAlign(value as TimelineAlign)}
options={options}
aria-label="对齐方式"
className="w-40"
/>
<ScrollArea direction="horizontal" shadow={false} className="w-full">
<Timeline items={items} orientation="horizontal" align={align} className="min-w-lg" />
</ScrollArea>
</Stack>
)
}
尺寸
size 调整文字与节点间距。间距跟随密度变量,默认是 md。
sm
- 条目 A第一项的说明文字。
- 条目 B这一项包含更长的说明文字,换行后节点之间的连线仍然连续,后面的内容随之向下排列。
- 条目 C最后一项不继续绘制连线。
md
- 条目 A第一项的说明文字。
- 条目 B这一项包含更长的说明文字,换行后节点之间的连线仍然连续,后面的内容随之向下排列。
- 条目 C最后一项不继续绘制连线。
lg
- 条目 A第一项的说明文字。
- 条目 B这一项包含更长的说明文字,换行后节点之间的连线仍然连续,后面的内容随之向下排列。
- 条目 C最后一项不继续绘制连线。
import { SimpleGrid, Stack, Text, Timeline, type TimelineSize } from '@hina-ui/react'
const sizes: TimelineSize[] = ['sm', 'md', 'lg']
const items = [
{
id: 'a',
title: '条目 A',
description: '第一项的说明文字。',
time: '09:00',
dateTime: '2026-09-16T09:00:00+08:00',
},
{
id: 'b',
title: '条目 B',
description: '这一项包含更长的说明文字,换行后节点之间的连线仍然连续,后面的内容随之向下排列。',
time: '09:20',
dateTime: '2026-09-16T09:20:00+08:00',
},
{
id: 'c',
title: '条目 C',
description: '最后一项不继续绘制连线。',
time: '09:40',
dateTime: '2026-09-16T09:40:00+08:00',
},
]
export default function Demo() {
return (
<SimpleGrid min="12rem" className="w-full" gap="lg">
{sizes.map(size => (
<Stack key={size}>
<Text size="sm" tone="muted">
{size}
</Text>
<Timeline items={items} size={size} />
</Stack>
))}
</SimpleGrid>
)
}
自定义节点与内容
renderMarker 替换节点,可组合 Avatar、Spinner 或图标。renderContent 替换整个正文区域,包括默认时间、标题和描述;需要只改某一部分时,用 renderTime、renderTitle 或 renderDescription。
所有渲染函数接收 { item, index },item 保留自定义字段的类型,index 是当前显示顺序中的位置。示例使用 Card 和 Tag 组合正文,并在等待中的条目上显示加载指示。
已完成
完成内容区域可组合其他组件,节点可以换成头像。
Hina
已补充
完成通过插槽读取条目上的自定义字段。
Hina
进行中
进行中等待中的节点可以使用加载指示。
Hina
'use client'
import { Check } from 'lucide-react'
import {
Avatar,
Card,
Inline,
Spinner,
Stack,
Tag,
Text,
Time,
Timeline,
type TimelineItem,
} from '@hina-ui/react'
interface Item extends TimelineItem {
author: string
pending: boolean
}
const items: Item[] = [
{
id: 'a',
title: '已完成',
description: '内容区域可组合其他组件,节点可以换成头像。',
time: '09:00',
dateTime: '2026-09-16T09:00:00+08:00',
author: 'Hina',
pending: false,
tone: 'success',
},
{
id: 'b',
title: '已补充',
description: '通过插槽读取条目上的自定义字段。',
time: '09:20',
dateTime: '2026-09-16T09:20:00+08:00',
author: 'Hina',
pending: false,
tone: 'success',
},
{
id: 'c',
title: '进行中',
description: '等待中的节点可以使用加载指示。',
time: '09:40',
dateTime: '2026-09-16T09:40:00+08:00',
author: 'Hina',
pending: true,
tone: 'accent',
},
]
export default function Demo() {
return (
<Timeline
items={items}
className="max-w-xl"
renderMarker={({ item, index }) =>
index === 0 ? (
<Avatar name={item.author} size="sm" />
) : item.pending ? (
<Spinner size="sm" />
) : (
<Check />
)
}
renderContent={({ item }) => (
<Card>
<Stack gap="sm">
<Inline justify="between">
<Text weight="medium">{item.title}</Text>
<Tag tone={item.pending ? 'accent' : 'success'}>
{item.pending ? '进行中' : '完成'}
</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" className="text-muted text-xs" />
</Inline>
</Stack>
</Card>
)}
/>
)
}
反向顺序
reverse 反转实际渲染顺序,不修改传入的数组。连线终点、渲染函数收到的索引和交替位置都按显示顺序计算。
- 条目 A第一项的说明文字。
- 条目 B这一项包含更长的说明文字,换行后节点之间的连线仍然连续,后面的内容随之向下排列。
- 条目 C最后一项不继续绘制连线。
'use client'
import { useState } from 'react'
import { Stack, Switch, Timeline } from '@hina-ui/react'
const items = [
{
id: 'a',
title: '条目 A',
description: '第一项的说明文字。',
time: '09:00',
dateTime: '2026-09-16T09:00:00+08:00',
},
{
id: 'b',
title: '条目 B',
description: '这一项包含更长的说明文字,换行后节点之间的连线仍然连续,后面的内容随之向下排列。',
time: '09:20',
dateTime: '2026-09-16T09:20:00+08:00',
},
{
id: 'c',
title: '条目 C',
description: '最后一项不继续绘制连线。',
time: '09:40',
dateTime: '2026-09-16T09:40:00+08:00',
},
]
export default function Demo() {
const [reverse, setReverse] = useState(false)
return (
<Stack className="w-full" gap="lg">
<Switch checked={reverse} onCheckedChange={setReverse}>
反向排列
</Switch>
<Timeline items={items} reverse={reverse} />
</Stack>
)
}
RTL
方向继承外层的 dir,也可直接传入 dir="rtl"。纵向起始侧与结束侧、横向节点顺序都会跟随方向。
- 09:00البند الأولوصف موجز للبند الأول.
- 09:20البند الثانيوصف البند الثاني.
- 09:40البند الثالث
import { Timeline } from '@hina-ui/react'
const items = [
{ id: 'a', title: 'البند الأول', description: 'وصف موجز للبند الأول.', time: '09:00' },
{ id: 'b', title: 'البند الثاني', description: 'وصف البند الثاني.', time: '09:20' },
{ id: 'c', title: 'البند الثالث', time: '09:40' },
]
export default function Demo() {
return <Timeline items={items} dir="rtl" />
}
无障碍
- 根元素是有序列表,条目按 DOM 顺序阅读;可通过
aria-label命名。 - 节点和连线是装饰,对辅助技术隐藏。
renderMarker应只返回非交互内容;链接、按钮放在renderContent或renderOpposite中,保持正常键盘操作。 - 不增加焦点停靠点,也不自动赋予选中、完成或当前步骤语义。
- 默认不包含动画;自定义加载指示遵循其自身的减弱动态效果设置。
API
Props
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
items | T[] | 必填 | 按顺序排列的条目, T extends TimelineItem |
orientation | 'vertical' | 'horizontal' | 'vertical' | 排布方向 |
align | 'start' | 'end' | 'alternate' | 'start' | 节点轴与内容的相对位置 |
size | 'sm' | 'md' | 'lg' | 'md' | 文字与节点间距 |
tone | TimelineTone | 'accent' | 默认节点颜色 |
timePosition | 'content' | 'opposite' | 'content' | 默认时间的位置 |
reverse | boolean | false | 反转显示顺序 |
className | string | — | 根元素的类 |
原生属性(如 dir、aria-label、style)透传到根元素。
TimelineItem
| 字段 | 类型 | 说明 |
|---|---|---|
id | string | number | 稳定的条目标识;省略时使用显示索引 |
title | string | 标题 |
description | string | 描述 |
time | string | 直接显示的时间文字 |
dateTime | string | time 元素的 datetime 属性,不参与格式化或排序 |
tone | TimelineTone | 覆盖该项的节点颜色 |
TimelineTone:'accent' | 'neutral' | 'success' | 'warning' | 'danger' | 'info'。所有条目字段均可选,渲染函数可以完全接管呈现。
内容属性
| 属性 | 参数 | 说明 |
|---|---|---|
renderMarker | { item: T, index: number } | 节点装饰,默认空心圆点 |
renderContent | { item: T, index: number } | 整个正文区域 |
renderTitle | { item: T, index: number } | 标题 |
renderDescription | { item: T, index: number } | 描述 |
renderTime | { item: T, index: number } | 时间,位置由 timePosition 决定 |
renderOpposite | { item: T, index: number } | 对侧区域,优先于对侧时间 |
同时导出 TimelineItem、TimelineSlotProps<T>、TimelineTone、TimelineSize、TimelineOrientation 和 TimelineAlign 类型。