跳到主要内容
menumenu tanreiexpand_morecontrasttranslate
tanreiexpand_more
开始使用WebReactSwiftUIJetpack ComposeFlutterReact Native预设你自己的设计系统与 AI 代理一起使用 Tanrei品牌设计令牌设计系统与定制Theme Studio颜色字体排印图标形状层级动效交互状态行为布局无障碍文字颜色令牌字阶令牌形状令牌层级令牌动效令牌状态令牌间距与度量令牌组件按钮主操作按钮分段按钮按钮组带菜单的按钮工具栏复制按钮复选框单选按钮开关纸片(Chip)菜单滑块滑块字段日历DateTimePicker字段组单选组复选框组下拉选择框下拉选择字段菜单栏文件上传文件选择器 进阶放置区文本输入框 进阶文本字段纸片字段字段组合框 进阶组合框字段数字输入框 进阶数字字段颜色输入框 进阶颜色字段一次性验证码输入框 进阶一次性验证码字段嵌入式文本输入框 进阶聊天输入栏卡片分隔线模态层 进阶对话框列表列表行底部面板侧边面板轮播代码块表格弹出框步骤折叠面板描述列表图示应用外壳页头树视图时间线图片预览调整手柄可调整大小的框滚动区域徽标进度指示器计量条数字滚动器加载指示器消息条(Snackbar)工具提示键盘快捷键标签(Tag)行内代码提示块(Callout)头像空状态标签组(Tag group)骨架屏文件项色块跑马灯文本标签页标签页视图应用栏导航栏导航抽屉搜索链接分页上一页/下一页链接步骤条面包屑目录链接卡片FAB 菜单状态Tanrei 是如何构建的参与贡献许可与致谢
首页组件时间线

时间线

按时间顺序排列的事件:每个事件有一个标记、它的时间、一个标题和由你的内容构成的正文,并用一道细线与下一个事件相连。事件有状态时,由标记的形状和你写的文字说明哪些已经完成、哪个正在进行、哪些还在后面。它是供人阅读的列表,从不是控件。

content_copy以 Markdown 格式查看
类别
容器
令牌
comp.timeline
层级
基础

概览

Web
tanrei-timeline
schedule
SwiftUI
TanreiTimeline
radio_button_unchecked
Jetpack Compose
TanreiTimeline
radio_button_unchecked
Flutter
TanreiTimeline
radio_button_unchecked
React Native
Timeline
radio_button_unchecked

生成自 docs/status/status.json, specs/timeline.spec.json

对已发生之事的记录

  1. 按级别命名的文本样式

    文本样式改为按级别命名,每个圆角都可以做平滑处理。

  2. 可以扩展的词汇

    设计层可以给一个序列加一级,或给一条轴加一个值,并写明理由。

  3. 开发启动

    令牌管线、规格的 schema 和第一份规格已经就绪。

有三样东西都画成由一条线连起来的标记。它们的区别在于读者拿它们做什么:

组件 它是什么 标记 关于状态说了什么 是否接受输入
步骤 需要一步步照着做的一套操作流程 写着步骤编号的圆环 什么也不说 从不
时间线 对已经发生和将要发生之事的记录 一个圆点或一个字形,从不是编号 可以说明每个事件进行到了哪里 从不
步骤条(stepper) 用户在产品所运行的流程中走到了哪里 一个编号或一个对勾 用户自己所在的位置 是

时间线从不给事件编号、排序或隐藏事件,也不读取事件的时间。顺序就是你给出的顺序。

用法

在 Web 上,时间线是加在你自己的有序列表上的一个类,而不是一个元素。在任何脚本运行之前,这个列表就是文档里的列表,带着它的 <time> 元素;没有样式表时,它就是浏览器的编号列表,里面是一段段带日期的文字。

SwiftUI、Compose、Flutter 和 React Native 尚在计划中:本页开头各平台旁边的名称是规格为它们定下的,这些平台目前还没有提供任何实现。React 也还没有 Timeline 组件,请使用加了这个类的列表。

<link rel="stylesheet" href="@tanrei/web/tokens.css" />
<link rel="stylesheet" href="@tanrei/web/icons.css" />
<link rel="stylesheet" href="@tanrei/web/timeline.css" />

<!-- 日志:只是发生过的事件。 -->
<ol class="tanrei-timeline" role="list" aria-label="发布历史">
  <li>
    <time class="tanrei-timeline-time" datetime="2026-09-19">2026 年 9 月 19 日</time>
    <p class="tanrei-timeline-title">开发启动</p>
    <p>令牌管线、规格的 schema 和第一份规格已经就绪。</p>
  </li>
</ol>

<!-- 计划:每个事件都有状态,用形状表示,也用你写的文字表示。 -->
<ol class="tanrei-timeline" role="list" data-size="large" aria-label="路线图">
  <li data-status="done" data-status-label="已完成">
    <!-- 不是一个时刻:用段落,而不是 time 元素。 -->
    <p class="tanrei-timeline-time">Wave 1</p>
    <!-- 标题同时是页面的标题元素:它保留自己的角色和级别。 -->
    <h3 id="wave-1">第一天就需要的控件</h3>
    <p>按钮、字段、选择控件和列表。</p>
  </li>
  <li data-status="current" data-status-label="进行中">
    <p class="tanrei-timeline-time">Wave 2</p>
    <h3 id="wave-2">导航和浮层</h3>
  </li>
  <li data-status="upcoming" data-status-label="未开始">
    <!-- 标记里放你自己的字形。 -->
    <span class="tanrei-timeline-marker tanrei-icon" aria-hidden="true">flag</span>
    <p class="tanrei-timeline-time">Wave 3</p>
    <h3 id="wave-3">专门用途的部件</h3>
  </li>
</ol>

<script type="module">
  // 可选:把唯一正在进行的那个事件标记为列表的当前项,并加上调试报告。
  // `@tanrei/web` 也会加载它。
  import '@tanrei/web/timeline';
</script>
import '@tanrei/web/timeline.css';
import '@tanrei/web/timeline';

<ol className="tanrei-timeline" role="list" aria-label="发布历史">
  <li>
    <time className="tanrei-timeline-time" dateTime="2026-09-19">2026 年 9 月 19 日</time>
    <p className="tanrei-timeline-title">开发启动</p>
    <p>令牌管线、规格的 schema 和第一份规格已经就绪。</p>
  </li>
</ol>
属性 在 Web 上
label aria-label(或者用 aria-labelledby 指向列表前面的标题元素)
size: large data-size="large"
density: compact data-density="compact"
一个事件 <li>
带 datetime 的 time <time class="tanrei-timeline-time" datetime="…">,里面是你写的文字
不是一个时刻的 time <p class="tanrei-timeline-time">
title 带 class="tanrei-timeline-title" 的元素(一个 <p>)
标题插槽 位于事件开头或紧跟在时间之后的标题元素(h2 到 h6)
status data-status="done"、"current" 或 "upcoming"
statusLabel data-status-label
icon 作为列表项第一个子元素的 <span class="tanrei-timeline-marker tanrei-icon" aria-hidden="true">

请在列表上写 role="list":有一种浏览器引擎会在列表的标记被去掉后丢掉它的列表语义。

时间由你来写。用读者的语言、历法和格式来写,并把同一个时刻放进 datetime,供机器读取。时间线不做任何格式化:“2 小时前”会一直是写下的样子,直到你改动它。

规格

计划:已完成、已完成、进行中、还在后面

  1. Wave 0

    基础

    令牌、主题、字体排印、图标、表面和状态的基础构件。

  2. Wave 1

    第一天就需要的控件

    按钮、字段、选择控件和列表。

  3. Wave 2

    导航和浮层

    抽屉、标签页、对话框、菜单和面板。

  4. Wave 3

    专门用途的部件

    选择器、轮播,以及文档网站需要的东西。

你自己的字形,以及紧凑的日志

  1. 周一 09:41

    已下单

  2. 周二 14:05

    运输中

  3. 预计周四

    送达

  1. 已保存设计层

  2. 已在六种配色方案中检查对比度

  3. 已更改主色

  • 标记位于起始侧边缘的一个框里,中号时宽 24,大号时宽 28,与事件的第一行居中对齐。没有状态时,它是一个小圆点;放了你自己的字形时,它是一个用细线画出的圆环,里面是这个字形。
  • 状态只改变标记的形状,不改变别的:已完成是里面有对勾的圆环,正在进行的事件是里面有圆点的填充圆,还在后面的事件是空的圆环。除了时间的颜色之外,这个填充圆是时间线带来的唯一颜色。
  • 连接线从每个标记的框通向下一个,两端都不与框相接。最后一个事件没有连接线;无论事件处于什么状态,事件之间都是同一种线。
  • 时间位于事件的最前面,用低调的颜色。标题跟在后面,字号与正文相同,字重是标题的字重。正文是你的内容:两个块之间相距 8。
  • 密度:regular 在两个事件之间留 16,compact 留 8,用于由单行事件组成的长日志。它只改变这一段间距。
  • 尺寸就是长文排版的那两种:在控件之间用中号,在以文本为主的页面上用大号。标记和连接线与步骤的一致,所以两者在同一个页面上看起来同属一个系列。

构成

部件 说明
1 list The ordered list itself: one column of events, as wide as its container gives it. It has no fill, no edge, no padding and no state of its own.
2 item One event: its marker at the start, and beside it its content — a time, a title and a body, each optional. Nothing about an event answers a pointer, a press or focus.
3 marker What begins an event, centred in a box at the start edge. A small filled dot for an event with no status; a hairline ring holding a glyph where the product names one; and for an event with a status a shape of its own: a ring holding a check for done, a filled circle holding a dot for the one under way, an empty ring for one still ahead. Decorative: the status is given in words.
4 connector · 可选 The line from an event's marker to the next event's: the system's one hairline, on the marker column's centre line, kept a small distance from both markers. It shows that the events are one course of time. Decorative. The last event has none.
5 time · 可选 When the event happened or is due, as the product writes it ("19 September 2026", "09:41", "Q3", "Wave 1"): a short line in a quiet color, first in the event's content.
6 title · 可选 What the event is, in a few words, set at the body's size in a heading weight. Text of its event and not a heading, unless the product gives a heading of its own as the title.
7 body · 可选 What more there is to say: the product's content. Text placed directly takes the body's text style; long-form blocks are set as prose; a block that is not text takes the body's whole width.

属性

属性 类型 默认值 说明
label string "" The list's accessible name, in the product's language ("Release history"). Not drawn. Give one where no heading directly before the timeline already says what it records.
size medium | large "medium" The size of the events' text, in prose's two sizes. Medium sets the body in the interface's own text style, for a timeline among controls: a panel, a card, a record's activity. Large sets it in the style for long reading, for a page whose purpose is the text: a changelog, a plan. A timeline inside a prose is given that prose's size. A design system may add values.
density regular | compact "regular" How far apart the events are. Regular is for events with a body, or few of them. Compact halves the space between events, for a long log of one-line events. It changes that space only. A design system may add values.

条目

该组件重复排列的条目种类。测试样例按顺序列出条目;各平台以各自的惯用方式为每个种类命名。

event

One event of the timeline: a time, a title, a body, and where the events have them a status and a glyph. Its place comes from the order the product gives the events in.

属性 类型 默认值 说明
time string "" When, as the product writes it for its reader, in the product's language, calendar and format ("19 September 2026", "2 hours ago", "Q3", "Wave 1"). Drawn first in the event. The timeline does not format, translate or compare it.
datetime string "" The same moment for a machine: a date, a time or a period in the form the platform's time element takes (ISO 8601: "2026-09-19", "2026-09-19T09:41:00Z"). Not drawn. With it the time is exposed as a time; without it, as text — which is right for a time that is not a moment ("Wave 1").
title string "" What the event is, in a few words. Plain text: read with the event, and not a heading. A title that is a heading of the page is given in the title slot instead.
status none | done | current | upcoming "none" Where the event stands, for a timeline whose events have a state: done (finished), current (under way now), upcoming (not begun). None, the default, is an event that simply happened: a log has no statuses. It changes the marker's shape and nothing else; the words are statusLabel's.
statusLabel string "" The status in words, in the product's language ("Done", "In progress", "Planned"), read by assistive technology before the event's time and title and not drawn. Needed with every status but none, unless the event's own text already says it. Ignored while status is none.
icon string "" A glyph of the system's icon set, by name, drawn in the marker in place of the dot, the check or the inner mark (a release's tag, a comment's bubble). Decorative.
插槽 说明
title The title as content of the product's own: a heading of the level the page's outline needs (so the event is in the page's table of contents and can be linked to), or text with a link or a tag in it. Drawn in the title's text style whatever its level; its role and level are kept. Takes the place of the title prop.
规格 Web SwiftUI Jetpack Compose Flutter React Native
条目 li TanreiTimelineEvent TanreiTimelineEvent TanreiTimelineEvent TimelineEvent
time · 属性 tanrei-timeline-time time time time time
datetime · 属性 datetime date dateTime dateTime dateTime
title · 属性 tanrei-timeline-title title title title title
status · 属性 data-status status status status status
statusLabel · 属性 data-status-label statusLabel statusDescription statusSemanticLabel statusAccessibilityLabel
icon · 属性 tanrei-timeline-marker icon icon icon icon
备注 An <li> of the list. The time is the item's <time class="tanrei-timeline-time" datetime="…">, or a <p> of that class where it is not a moment. The title is the next child where it is a heading (h2 to h6) or carries class="tanrei-timeline-title": the spec's title prop and its title slot are the same element on this platform, told apart by the tag the product chose. Every other child is the body. data-status is done, current or upcoming; data-status-label carries the words. A glyph is <span class="tanrei-timeline-marker tanrei-icon" aria-hidden="true">flag</span> as the item's first child. TanreiTimelineEvent(title:, time:, date:, status:, statusLabel:, icon:) { body }; a heading title is the title: view builder, marked with the header trait by the product. TanreiTimelineEvent(title, time, dateTime, status, statusDescription, icon) { body } inside the timeline's scope. TanreiTimelineEvent(title:, time:, dateTime:, status:, statusSemanticLabel:, icon:, child:); titleWidget: takes a heading of the product's own. <TimelineEvent title="…" time="…" status="done" statusAccessibilityLabel="Done">body</TimelineEvent>.

状态

行为

以 EARS 形式写成的需求。每个平台的实现都按这些 ID 逐条测试。需求原文取自规格文件,原样以英文呈现。

ID 需求
TML-1 THE timeline SHALL lay out its events in one column, in the order they are given, each event a row of two parts: its marker, in a column at the timeline's start edge, and its content — its time, its title and its body, those it has, in that order — beside it. The marker column SHALL be as wide as the marker's box of the size (comp.timeline.marker.height.medium for medium, comp.timeline.marker.height.large for large); every event's content SHALL begin at one edge, comp.timeline.marker.gap after that column, and run to the timeline's end edge. THE timeline SHALL draw no fill, no edge and no elevation of its own and SHALL add no padding around itself.
TML-2 THE timeline SHALL separate the end of one event from the beginning of the next by the item gap of its density (comp.timeline.item.gap.regular for regular, comp.timeline.item.gap.compact for compact), and SHALL change nothing else with density. THE timeline SHALL keep the order the product gives: it SHALL NOT sort, reverse, group, number, hide, collapse or page its events, and SHALL NOT read their times to do so.
TML-3 WHERE an event has a time THE timeline SHALL draw it first in the event's content, as the product wrote it, in the time style of its size (comp.timeline.time.typescale.medium for medium, comp.timeline.time.typescale.large for large) and in comp.timeline.time.color, wrapping and never cut, and SHALL begin what follows it in the event comp.timeline.time.gap below it. WHERE datetime is not empty THE timeline SHALL expose the time as a time whose machine-readable value is datetime, and SHALL NOT draw datetime; WHERE it is empty the time is text. THE timeline SHALL NOT format, translate, shorten or update a time: a time that changes as the clock moves ("2 hours ago") is the product's to change.
TML-4 WHERE an event has a title THE timeline SHALL draw it after the time, in comp.timeline.title.typescale.medium for medium (comp.timeline.title.typescale.large for large) and the content color of the surface the timeline sits on, SHALL wrap it and never cut it, and SHALL begin the event's body comp.timeline.title.gap below it. A title given as text SHALL be exposed as text of its event and SHALL NOT be a heading; WHERE the product gives the title as content of its own in the event's title slot THE timeline SHALL draw it as any title, in the same text style whatever its level, in place of the title prop, and SHALL change neither its role nor its level, nor the roles of what it holds (a link, a tag). THE timeline SHALL set the title's letter-spacing by the script of its text, as every title is set (PRS-20): no negative letter-spacing WHERE its language is Chinese, Japanese or Korean, and none WHERE its script joins its letters or stacks marks on them.
TML-5 THE timeline SHALL draw the content an event is given as that event's body and SHALL give it comp.timeline.body.typescale.medium for medium (comp.timeline.body.typescale.large for large) as the text style its text takes where nothing inside sets another, and no color of its own: its text takes the content color of the surface the timeline sits on. The body SHALL wrap and SHALL never be cut or limited to a number of lines. WHERE the body holds more than one block THE timeline SHALL separate two blocks that follow each other by comp.timeline.body.gap, SHALL lay long-form blocks out as prose of the timeline's size, and SHALL give every block that is not text — a code block, a table, a picture, any component — the body's whole inline size, such a block scrolling inside its own box when its content cannot shrink (PRS-15).
TML-6 THE timeline SHALL draw each event's marker centred in a square box whose side is the marker's box of the size (TML-1), the box at the top of the event with its centre on the centre of the event's first line of text; WHERE the box is taller than that line THE timeline SHALL lower the event's content by half the difference, so that no marker reaches into the space above its event. WHILE an event's status is none and its icon is empty THE marker SHALL be a filled circle of diameter comp.timeline.marker.dot.size in comp.timeline.marker.dot.color, a circle whatever corners a ring has. WHILE its status is none and its icon is not empty THE marker SHALL be a ring — the whole box, with corners comp.timeline.marker.shape, never larger than half the box's side and eased by sys.shape.corner-smoothing, no fill, and an edge comp.timeline.marker.outline.width wide in comp.timeline.marker.outline.color drawn inside the box — holding that glyph at its centre (TML-8). Every marker SHALL be kept from assistive technology.
TML-7 WHILE an event's status is done THE timeline SHALL draw its marker as the ring of TML-6 holding the icon set's check glyph, or the event's own glyph where icon is not empty, in comp.timeline.marker.icon.color. WHILE its status is current THE marker SHALL be the whole box filled with comp.timeline.marker.current.container.color, with the ring's corners and no edge, holding a filled circle of diameter comp.timeline.marker.dot.size, or the event's own glyph, in comp.timeline.marker.current.icon.color. WHILE its status is upcoming THE marker SHALL be the ring of TML-6, empty, or holding the event's own glyph in comp.timeline.marker.icon.color. The three SHALL differ in shape and not only in color — a mark in a ring, a filled circle, an empty ring — and a status SHALL change the marker only: the event's time, title, body and connectors are drawn as every other event's. Any number of events may have any status; THE timeline SHALL NOT derive one event's status from another's or from its time.
TML-8 WHERE a marker holds a glyph THE timeline SHALL draw it in a box of comp.timeline.marker.icon.size at the centre of the marker, in comp.timeline.marker.icon.color (comp.timeline.marker.current.icon.color on the marker of a current event), with sys.icon.family.unselected and sys.icon.fill.unselected, sys.icon.weight and an optical size clamped between sys.icon.optical-size.min and sys.icon.optical-size.max, and SHALL keep it from assistive technology.
TML-9 WHILE an event is followed by another event THE timeline SHALL draw a connector between their two markers: a straight line comp.timeline.connector.thickness thick in comp.timeline.connector.color, on the centre line of the marker column, from comp.timeline.connector.gap below the bottom of the upper marker's box to comp.timeline.connector.gap above the top of the lower marker's box, through the space between the two events, however tall the upper event's content is. THE timeline SHALL draw no connector after the last event and none for a timeline of one event; a connector SHALL take no room in the layout, SHALL never be drawn over a marker or over content, SHALL be the same line between events of any status, and SHALL be kept from assistive technology.
TML-10 THE timeline SHALL lay itself out as a block in the flow of its container: it SHALL take the whole inline size the container gives it, with its start edge on the start edge of the text around it; it SHALL bring no outer space of its own (PRS-15: the space between it and its neighbours is its container's), SHALL never be wider than its container, SHALL have no minimum width and no maximum width, and SHALL NOT scroll. WHILE the timeline sits in a prose that keeps a measure THE timeline SHALL take that prose's full width as a block that is not text (PRS-18).
TML-11 THE timeline SHALL NOT take pointer, touch or keyboard input, SHALL NOT take focus or be a stop in the focus order, and SHALL NOT change anything it draws under a pointer, a press or focus: an event has no hovered, focused, pressed, selected or disabled state, a marker is not a control, and the timeline emits no event of its own. What the product places in a title slot or a body — a link, a Button, a tag — takes input by its own spec, and text in a time, a title and a body SHALL remain selectable where the platform lets text be selected.
TML-12 THE timeline SHALL be exposed to assistive technology as an ordered list of as many items as it has events, named by label where label is not empty, and each event as one item of that list holding, in this order, its status in words, its time, its title and its body. WHILE an event's status is not none and its statusLabel is not empty THE timeline SHALL expose statusLabel as text before the event's time, set apart so that it is read as a phrase of its own ("Done, 19 September 2026, …"), and SHALL NOT draw it; WHILE status is none it SHALL ignore statusLabel. WHILE exactly one event of the timeline has the status current THE timeline SHALL also expose that event as the current item of its list, in the platform's own way; with none or several it SHALL expose no item as current. THE timeline SHALL NOT expose a status in any other way, and SHALL add no role to a time without datetime, to a title given as text or to anything in a body.
TML-13 WHEN label, or an event's time, datetime, title, status, statusLabel, icon or body changes, or an event is added, removed or moved, THE timeline SHALL draw and expose the new value at once, without a motion, and SHALL NOT announce the change by itself and SHALL NOT scroll to it.
TML-14 WHILE the text size is enlarged THE timeline SHALL grow each time, title and body with it, keep every marker's box at the marker's box of the size and the dot and the glyph at comp.timeline.marker.dot.size and comp.timeline.marker.icon.size, each still centred on its event's first line (TML-6), and keep comp.timeline.marker.gap, the item gap, comp.timeline.time.gap, comp.timeline.title.gap, comp.timeline.body.gap and comp.timeline.connector.gap as they are; it SHALL wrap, set no fixed height on an event, lose no content when the reader raises line height, paragraph spacing, letter spacing or word spacing, and SHALL need no horizontal scrolling at a width of 320 points with text at 200 %, except inside a block that scrolls in its own box (TML-5).
TML-15 WHERE the timeline is placed inside an event's body of another timeline THE timeline SHALL draw itself as a timeline of its own in the body's width, and in debug builds SHALL report that timelines are nested: events inside an event are a list of prose in the body.
TML-16 WHERE the platform draws with a person's own forced colors (the web's forced-colors mode, and a platform's equivalent where it has one) THE timeline SHALL draw each dot, each ring, each glyph and each connector in the platform's text color at their own sizes and widths, the marker of a current event as a circle filled with that text color whose inner mark or glyph is in the platform's background color, and each time, title and body in the text color; the three statuses SHALL then still differ by shape (TML-7), and nothing else the timeline draws relies on a fill.
TML-17 WHERE the layout direction is right-to-left THE timeline SHALL mirror its two parts and apply its spaces by logical start and end, so that the marker column and the connectors are at the right and each event's content is at its left, beginning at one edge and running to the timeline's left edge.
TML-18 THE timeline SHALL take its layout direction from where it is used — the direction of the nearest ancestor that sets one, and the platform's own where none does — and SHALL have no direction setting of its own; WHEN that direction changes while the timeline is shown THE timeline SHALL draw itself for the new direction at once, without a motion of its own, keeping what it shows.
TML-19 WHERE the layout direction is right-to-left THE timeline SHALL draw the glyph of a marker mirrored left to right exactly when the system's icon set marks that glyph's name as mirrored or the product forces mirroring for that use, and unmirrored when the product forbids it: the icon helper mirrors the glyph inside its own box, about the box's vertical centre line, changing neither its place, size, color and fill nor the order TML-17 gives. THE timeline SHALL NOT mirror a glyph itself. The dot, the ring and the check are never mirrored.
TML-20 THE timeline SHALL lay out an event's time, and a title given as text, each as text of its own direction, isolated from the text around it: it takes the base direction of its first strong character, or the layout direction where it has none, so that a date written in digits, a version and a clock time keep their own order in a right-to-left layout and reorder nothing beside them; THE timeline SHALL still place the time and the title and align their lines by the layout direction, not by the text's own. The body's direction is its content's.
TML-21 WHEN size or density names a value that the active design system does not have THE timeline SHALL render with the default value's tokens, and WHEN an event's status names a value this spec does not have THE timeline SHALL draw that event as for none; in debug builds it SHALL report the unknown value.
TML-22 IF the timeline holds no event THE timeline SHALL draw nothing, take no space and expose nothing: what stands in an empty timeline's place is the product's (an empty state). IF an event has neither a time, a title nor a body THE timeline SHALL still draw its marker and count it in the list's items, and in debug builds report that an event is empty. WHILE an event's status is not none and its statusLabel is empty THE timeline SHALL still draw it and in debug builds report that a status is not given in words; IF statusLabel is not empty while status is none THE timeline SHALL ignore it and in debug builds report it.

各平台名称对照

对等并不意味着 API 完全相同。同一份契约,由各平台以自己的惯用方式表达。

规格 Web SwiftUI Jetpack Compose Flutter React Native
组件 tanrei-timeline TanreiTimeline TanreiTimeline TanreiTimeline Timeline
label · 属性 aria-label label label semanticLabel accessibilityLabel
size · 属性 data-size size size size size
density · 属性 data-density density density density density
备注 It is the form the steps take. A class on the product's own ordered list, not an element — <ol class="tanrei-timeline" role="list"> with <li> children — styled by one optional stylesheet of the package, @tanrei/web/timeline.css, whose selectors are wrapped in :where() so one class on the product's side wins. The list stays the document's list: it is an ordered list with <time> elements before any script runs and for whatever reads the page as built, a renderer's <ol> needs one class and no wrapper, and there is nothing to define, so nothing changes place after the page loads; without the stylesheet it is the browser's numbered list of dated paragraphs. label is aria-label (or aria-labelledby naming the heading before it); data-size="large" and data-density="compact" are the size and the density. The marker is the item's ::before — the dot, the ring, the filled circle with its inner dot, and the done event's check as the icon font's check ligature — unless the item's first child is a .tanrei-timeline-marker, which then is the marker and holds the glyph through the package's .tanrei-icon helper; the connector is the item's ::after, absolutely placed, with pointer-events: none. list-style: none removes list semantics in one engine's accessibility tree, which is why the contract writes role="list" on the <ol>. The status in words (TML-12) is generated content of the item's first text part, content: attr(data-status-label) ", ", clipped out of sight and read with the item; the test of TML-12 is run with a screen reader in each engine, and where an engine does not read it the package's script writes a visually hidden <span> instead. The same script, where it is loaded, sets aria-current="true" on the item while exactly one has data-status="current", and adds the debug reports of TML-15, TML-21 and TML-22; without it no item is exposed as current, and the words still are. A time and a title are dir="auto"-like through unicode-bidi: plaintext with physical alignment by :dir() (TML-20). TML-6 uses 1lh of the item's first line. Inside a .tanrei-prose the list is not an island (it is a class on an <ol>), so prose's own rules keep reaching each body, and timeline.css takes back from prose.css what a timeline draws itself: the list's inset, its markers, the item gap and the measure on the list. Under forced-colors: active the markers and the connector are CanvasText, and the current marker a CanvasText disc with a Canvas mark (TML-16). React: Timeline and TimelineEvent render the same <ol> and <li>; TimelineEvent takes time, dateTime, title (text, rendered as <p class="tanrei-timeline-title">) or a heading child, status, statusLabel and icon. TanreiTimeline(label:, size:, density:) { TanreiTimelineEvent(…) … }. A VStack of rows, not a List (no row chrome, no selection). Each event is one accessibility element that contains its children, whose label begins with the status label; date is a Date handed to the element's accessibility value where the platform has a use for it. TanreiTimeline(label, size, density) { TanreiTimelineEvent(…) … }. A Column with collectionInfo and collectionItemInfo semantics, so the events are a list of that many items; the connector is drawn behind the rows with drawBehind. No clickable and no focusable modifier. TanreiTimeline(semanticLabel:, size:, density:, events: [TanreiTimelineEvent(…), …]). A Column, not a ListView: the timeline does not scroll (TML-10). The connector is a CustomPaint behind the rows. Each event is a Semantics(container: true) whose label begins with the status label. <Timeline accessibilityLabel="Release history"> with <TimelineEvent> children; accessibilityRole="list" on the container. Each event is one accessible element whose label is composed from the status label, the time and the title. Follows the app's direction where an ancestor's is not known.

令牌

令牌 引用 解析值(浅色)
--tanrei-comp-timeline-body-gap sys.space.space100 8px
--tanrei-comp-timeline-connector-color sys.color.outline-variant #000000 · 8%
--tanrei-comp-timeline-connector-gap sys.space.space50 4px
--tanrei-comp-timeline-connector-thickness sys.border.hairline 1px
--tanrei-comp-timeline-item-gap-compact — 8px
--tanrei-comp-timeline-item-gap-regular — 16px
--tanrei-comp-timeline-marker-current-container-color sys.color.primary #1b1b1b
--tanrei-comp-timeline-marker-current-icon-color sys.color.on-primary #ffffff
--tanrei-comp-timeline-marker-dot-color sys.color.on-surface-variant #4b4b4b
--tanrei-comp-timeline-marker-dot-size sys.space.space100 8px
--tanrei-comp-timeline-marker-gap sys.space.space150 12px
--tanrei-comp-timeline-marker-height-large — 28px
--tanrei-comp-timeline-marker-height-medium — 24px
--tanrei-comp-timeline-marker-icon-color sys.color.on-surface-variant #4b4b4b
--tanrei-comp-timeline-marker-icon-size sys.icon.extra-small 16px
--tanrei-comp-timeline-marker-outline-color sys.color.outline-variant #000000 · 8%
--tanrei-comp-timeline-marker-outline-width sys.border.hairline 1px
--tanrei-comp-timeline-marker-shape sys.shape.corner.full 9999px
--tanrei-comp-timeline-time-color sys.color.on-surface-variant #4b4b4b
--tanrei-comp-timeline-time-gap sys.space.space25 2px
--tanrei-comp-timeline-title-gap sys.space.space50 4px
--tanrei-comp-timeline-body-typescale-large sys.typescale.body-large 400 16px/1.625
--tanrei-comp-timeline-body-typescale-medium sys.typescale.body 400 14px/1.5714
--tanrei-comp-timeline-time-typescale-large sys.typescale.body 400 14px/1.5714
--tanrei-comp-timeline-time-typescale-medium sys.typescale.body-small 400 13px/1.5385
--tanrei-comp-timeline-title-typescale-large sys.typescale.heading-5 500 16px/1.5
--tanrei-comp-timeline-title-typescale-medium sys.typescale.heading-6 500 14px/1.4286

测试样例

作为基准的渲染结果。所有平台都以相同的 ID 为每个样例截取快照,以便相互比较。

生成自 specs/timeline.spec.json, tokens/comp/timeline.tokens.json, design/tokens/

在长文排版中

在 .tanrei-prose 内部,时间线不是一座孤岛:每段正文里的段落、列表、链接和行内代码仍然归长文排版管。时间线占满长文排版的整个宽度,所以正文里的代码块会占满整栏,而正文里的段落仍然受行长限制。@tanrei/web/prose.css 已经带有时间线的规则,所以长文排版的页面不需要第二份样式表。

Web 上绘制方式的不同之处

  • 请把事件的文本写在元素(<p>)里:直接写在列表项里的文本,无法得到时间或标题下方的那段间隙。
  • 已完成事件的对勾是图标集的 check,所以它需要 icons.css 和图标字体。没有它们时,圆环是空的。
  • 在长文排版之外,长篇的正文(列表、引文)按它在你页面上任何地方的样子排版,而不是按长文排版来排。
  • 设计系统新增的尺寸或密度,在该系统为它提供规则之前,按默认值绘制。
  • 没有这个包的脚本时,不会有任何项被标记为列表的当前项。状态仍然会用文字朗读出来。

指南

推荐

  • 按读者预期的顺序给出事件:更新日志从新到旧,计划从旧到新。时间线会保持这个顺序。
  • 给每个有状态的事件在 data-status-label 里写上状态的文字,用读者的语言。
  • 当状态对每一位读者都重要时,也用文字把它显示出来:在标题或正文里放一个标签(Tag)。
  • 给相对时间(“2 小时前”)在 datetime 里写上确切的时刻。
  • 由单行事件组成的长日志请使用 compact,并在列表后面放一个 Button,用来显示更早的事件。

避免

  • 不要用时间线来写操作流程。步骤会给读者要做的事编号。
  • 不要用它来显示用户在自己正在走的流程中的位置:那是步骤条,而标记不是控件。
  • 不要把每个标题都写成标题元素。一份有四十个事件的更新日志会把页面大纲塞满。当每个事件都是页面的一个部分时,才使用标题元素。
  • 不要在事件里嵌套时间线。事件里的事件是正文里的一个列表。
  • 不要只靠标记来说明某件事失败了。请用事件的文字和一个标签(Tag)来说明。

无障碍

角色
list
标注
An ordered list, named by label where the product gives one; where a heading directly before the timeline says what it records, no label is needed. Each event is a list item read as its status in words (statusLabel, where it has a status), its time, its title and its body, in that order. A time with datetime is exposed as a time; the markers and the connector are decorative and hidden. A title is text of its item, or the product's own heading where the product gave one. Where exactly one event is current, it is the list's current item.
最小目标
即使外观尺寸更小,点击区域也满足最小目标尺寸。 minimum 32px · pointer 44px · spacing 8px · touch 48px

键盘操作

按键 动作

备注

  • Not interactive: the timeline takes no input and no focus (TML-11), so there is no target of its own to size and minimumTarget holds trivially. What is pressable inside a title or a body meets the minimum target by its own spec.
  • A status never rests on color alone (WCAG 1.4.1), and not on shape alone either. For a reader who sees, the three statuses are three shapes (TML-7), which forced colors keep (TML-16); for assistive technology each is statusLabel in words (TML-12). The component cannot tell whether an event's own text already says its status, so giving statusLabel is the product's to check. Where the status matters to every reader (a plan people decide by), show it in words too: a tag in the title or the body.
  • Order is carried by structure, not by the drawing: the timeline is an ordered list (TML-12), so a reader who does not see the markers and the connector loses nothing, and a missing stylesheet costs only the look.
  • A time is the product's words (TML-3). "2 hours ago" is read as written and goes stale as written; a product that shows relative times also gives datetime, so the exact moment is available to assistive technology and to the platform.
  • Contrast: the timeline brings no color for a title or a body, so they have the contrast of the content color of the surface they sit on. The time, the dot and a ring's glyph are on-surface-variant, held to 4.5:1 on every surface tone by the token invariants; the current marker's mark is on-primary on primary, held to 4.5:1, and primary on every surface tone is held to 4.5:1, as primary-colored text is, so the filled marker is found on any of them. The ring and the connector are the decorative hairline and are not held to a ratio: an upcoming event's empty ring may be faint, which is why its status is also in words.
  • A title is not a heading by default: a changelog of forty events would otherwise fill the page's outline. Where the events are the page's sections (one release per section), give each title as a heading in the title slot; its level is the page's, and the timeline does not change it (TML-4).
  • A timeline does not announce an event that is added (TML-13). A feed that grows while the person watches is the product's to announce, in a live region around or beside the timeline, and its own to decide whether to interrupt.
  • Text size, text spacing and reflow: TML-14. A long timeline is never cut, collapsed or paged by the component; hiding older events behind a control is the product's, with a Button after the list.

生成自 specs/timeline.spec.json, tokens/sys/measure.tokens.json

组件负责的部分

  • 它始终是文档里的有序列表:一个项数与事件数相同的列表,每一项按状态的文字、时间、标题、正文的顺序朗读。
  • 状态的文字在时间之前朗读,不会画在任何地方。标记和连接线是装饰性的,并且是隐藏的。
  • 三种状态是三种形状,而不是三种颜色,在强制颜色模式下也是如此。
  • 恰好只有一个事件正在进行时,这个包的脚本会把它标记为列表的当前项。
  • 带 datetime 的时间就是文档自己的 time 元素。
  • 文本随读者的设置放大并换行。任何内容都不会被截断、折叠或分页。

需要你做的事

  • 在列表上写 role="list",并用它前面的标题或 aria-label 为它命名。
  • 每个状态都要给出 data-status-label,除非事件自己的文本已经说明了这一点。
  • 在三种浏览器引擎的当前版本中,这些文字是通过样式表传给屏幕阅读器的。如果要在没有这个包的脚本的情况下支持较旧的浏览器,请自己把它们写成列表项开头的 <span class="tanrei-timeline-status">已完成,</span>,以代替 data-status-label。
  • 用户正看着的时候新增的事件,要在你自己的实时区域(live region)里朗读出来:时间线不会替你做。