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

消息条(Snackbar)

在屏幕底边或顶边的末端简短地告诉用户应用刚刚做了什么,最多带一个操作,不夺取焦点,也不打断用户的工作。

content_copy以 Markdown 格式查看
类别
信息传达
令牌
comp.snackbar
层级
基础

概览

Web
tanrei-snackbar
schedule
SwiftUI
TanreiSnackbar
schedule
Jetpack Compose
TanreiSnackbar
schedule
Flutter
TanreiSnackbar
schedule
React Native
Snackbar
schedule

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

规格已修订,尚未覆盖所有平台

本页描述的是规格当前所定的消息条(Snackbar):一个绘制在当前配色方案的表面上或以反色绘制的条,一个低调的 tonal 操作,用淡色呈现的状态形态,一个标题,以及可以展开的文本。Web 把这些全都画了出来。在它的反色状态形态通过评审、基准图记录完成之前,它在上表中仍标记为“待跟进”。SwiftUI、Compose、Flutter 和 React Native 提供的是较早的一版:操作是 text 样式,没有状态形态,没有标题,不能展开,也没有 inverse;在这些平台跟上之前,它们的示例展示的都是那一版。演示是目前发布的 Web 组件。

就地绘制:一条带操作的消息,和一条带关闭按钮的消息

用法

消息条不由你来摆放。你请求宿主显示一条消息条,宿主负责让它排队,把它放在自己区域的底部或顶部末端,为它计时并朗读它。那条消息条关闭后,show 才给出结果:如果它的操作被激活,结果是 action;否则是 dismiss 和原因。所以“撤销”的代码可以从上到下顺着读。

平台 宿主 显示消息条
Web <tanrei-snackbar-host>,作为页面的最后一个元素 host.show(options),返回 promise
SwiftUI TanreiSnackbarHost,用 .tanreiSnackbarHost(_:) 附着 await host.show(message:…)
Compose TanreiSnackbarHost(hostState),与内容同处一个 Box 并排在最后 hostState.showSnackbar(message, …),一个挂起调用
Flutter TanreiSnackbarMessenger,包在它所报告的页面外面 TanreiSnackbarMessenger.of(context).show(TanreiSnackbar(…)),返回一个控制器,它的 closed 是一个 future
React Native 由 TanreiProvider 在它的浮层中渲染 useSnackbar().show(options),返回 promise

每个宿主还可以关闭正在显示的消息条(dismissCurrent),也可以连同等待中的消息条一并丢弃(clear),两者的原因都是 host。绘制在宿主之外的消息条元素会就地绘制,并像宿主那样在给定的宽度内居中(placement 为 top-end 时,则放在该宽度的末端):没有超时、队列、入场动效和朗读。这种形式用于文档页面和测试,例如上面的演示。

<!-- 放在页面最后:宿主在焦点顺序中的位置,就是消息条的控件所在的位置。 -->
<tanrei-snackbar-host></tanrei-snackbar-host>

<script type="module">
  import '@tanrei/web/components/snackbar';
  const snackbars = document.querySelector('tanrei-snackbar-host');

  async function archive() {
    await archiveConversation();
    // 关闭后才给出结果:{ result: 'action' } 或 { result: 'dismiss', reason }。
    const { result } = await snackbars.show({ message: '会话已归档', action: '撤销' });
    if (result === 'action') await restoreConversation();
  }

  // 状态形态:有图标,并用文字向辅助技术说明状态。
  snackbars.show({
    status: 'error',
    statusLabel: '错误',
    title: '消息未发送',
    message: '你处于离线状态。消息会留在发件箱中。',
    action: '重试',
    expandLabel: '显示完整消息',
  });

  // 以反色绘制,不论设计系统如何选择。省略 `inverse` 则采用系统的选择。
  snackbars.show({ message: '链接已复制', inverse: true });
</script>
import { useRef } from 'react';
import { SnackbarHost } from '@tanrei/react';
import type { TanreiSnackbarHost } from '@tanrei/web';

// ref 就是元素本身:show() 是它的方法。
const snackbars = useRef<TanreiSnackbarHost>(null);

async function archive() {
  await archiveConversation();
  const { result } = await snackbars.current!.show({ message: '会话已归档', action: '撤销' });
  if (result === 'action') await restoreConversation();
}

// 状态形态,消息上方带有标题。
snackbars.current?.show({ status: 'error', statusLabel: '错误', title: '消息未发送', message: '你处于离线状态。', action: '重试' });

// 以反色绘制,不论设计系统如何选择。
snackbars.current?.show({ message: '链接已复制', inverse: true });

// 放在页面最后,位于内容之后。
<SnackbarHost ref={snackbars} />
import Tanrei

@State private var snackbars = TanreiSnackbarHost()

// 把宿主附着在要使用其底边的视图上。它绘制在一个浮层中,而不是 sheet 中。
ContentView()
    .tanreiSnackbarHost(snackbars)

// 消息条关闭后才返回:.action 或 .dismissed(reason)。
// 如果宿主的视图不在屏幕上,对它的请求会等到视图出现为止。
let result = await snackbars.show(message: "会话已归档", actionLabel: "撤销")
if case .action = result { restoreConversation() }
import io.github.chnotchy.tanrei.components.TanreiSnackbarHost
import io.github.chnotchy.tanrei.components.TanreiSnackbarHostState
import io.github.chnotchy.tanrei.components.TanreiSnackbarResult

// 与 Material 的 SnackbarHostState 相同:showSnackbar 会挂起,直到消息条关闭。
val snackbars = remember { TanreiSnackbarHostState() }
val scope = rememberCoroutineScope()
Box(Modifier.fillMaxSize()) {
    Content(onArchive = {
        scope.launch {
            when (snackbars.showSnackbar("会话已归档", actionLabel = "撤销")) {
                TanreiSnackbarResult.ActionPerformed -> unarchive()
                is TanreiSnackbarResult.Dismissed -> Unit
            }
        }
    })
    // 放在 Box 的最后:绘制在内容之上,在焦点顺序中排在最后。
    TanreiSnackbarHost(snackbars, Modifier.fillMaxSize())
}
import 'package:tanrei/tanrei.dart';

// 与 ScaffoldMessenger 相同。把它包在页面外面、放在页面的路由之内,这样在上方打开的对话框会盖住它。
TanreiSnackbarMessenger(child: page);

Future<void> archive(BuildContext context) async {
  await archiveConversation();
  final TanreiSnackbarController snackbar = TanreiSnackbarMessenger.of(context).show(
    const TanreiSnackbar(message: '会话已归档', actionLabel: '撤销'),
  );
  // 以 action、close、escape、timeout 或 host 完成。
  if (await snackbar.closed == TanreiSnackbarClosedReason.action) {
    await restoreConversation();
  }
}
import { useSnackbar } from '@tanrei/react-native';

// 宿主位于 TanreiProvider 的浮层中:请把 provider 放在应用的根部。
const snackbar = useSnackbar();

async function archive() {
  await archiveConversation();
  const { result } = await snackbar.show({ message: '会话已归档', actionLabel: '撤销' });
  if (result === 'action') await restoreConversation();
}

规格

一条换行的消息,和一个独占一行的操作

构成

构成

绘制在 Web 包所绘的测试样例 message 之上。
部件 说明
1 container The bar that holds the text and the controls, raised above the content of its host. Its color is its form's: a surface of the scheme (the default form, or a faint tint of a status) or, drawn inverse, the opposite scheme's surface or, in a status form, the bar the other theme draws for that status: deep in a light scheme, pale in a dark one.
2 outline · 可选 The edge of a status form: a hairline in the status's color, at the edge's opacity, around the bar where the ring of its elevation is. The default form and the inverse form draw none and keep that ring alone.
3 icon · 可选 The status glyph from the system's icon set, before the text: info, warning, error or check_circle. Drawn only in a status form, a little softer than the text (in the status's own color on a surface and, on an inverse bar, in the status color of the other theme, at the glyph's opacity), and hidden from assistive technology, which hears statusLabel instead.
4 title · 可选 A few words that name what happened, above the message. Up to three lines while collapsed.
5 supporting-text The message: what happened, or with a title the description, which is then drawn a little quieter than the title. Up to three lines while collapsed.
6 action · 可选 One control that undoes or follows up on what happened, drawn as a tonal button with the snackbar's own tokens, as tall as the small Buttons beside it: the color of its own bar with a tonal layer of its label's color over it, so that it stands one quiet step off the bar, the same step in every form.
7 expand · 可选 An icon-only Button of the small size that shows the whole title and message, and hides them again: the glyph expand_more while collapsed and expand_less while expanded. Drawn only while the text is longer than its collapsed limit, and drawn as the close button is (SNK-34).
8 close · 可选 An icon-only Button of the small size showing the icon set's close glyph, which closes the snackbar. The snackbar places it, chooses its variant (comp.snackbar.close-button.variant: tonal in the default system) and gives it its form's tone (BTN-45): the glyph in the bar's message color and, where the variant draws a container, a circle of that color made translucent. Everything else it reads from Button's own tokens.
9 focus-ring · 可选 Gap-then-ring indicator shown for keyboard focus of the action, or of the text while it scrolls, in the color its form gives it so that it shows on that form's bar. The expand and close buttons draw their own, in the same color (SNK-34).

尺寸

尺寸

长度 令牌 值(px)
container padding-inline-start tanrei.comp.snackbar.container.padding-inline-start 16
container padding-inline-end tanrei.comp.snackbar.container.padding-inline-end 16
绘制在 Web 包所绘的测试样例 message 之上。

属性

属性 类型 默认值 说明
message string "" What happened; with a title, the description. Required. Plain text: the whole message is announced, even when a collapsed snackbar cuts it.
title string "" A few words above the message that name what happened ("Message not sent"). Empty draws the message alone.
action string "" The label of the one action ("Undo", "View"), drawn as a tonal button that sits one quiet step off the bar, the same step in every form. Empty draws no action.
status default | info | warning | error | success "default" The form of the snackbar. Default is the neutral bar with no glyph; info, warning, error and success draw the bar as a quiet tint of that status (pale on the scheme's surface or, drawn inverse, the bar the other theme draws for that status, with that theme's text, glyph and edge colors), with an edge in the status's color and the status's glyph before the text, and warning and error are announced assertively.
statusLabel string "" The status in words, in the product's language ("Error"), announced before the title and the message so that the status never rests on color and glyph alone. Required while status is not default.
inverse boolean null Whether this snackbar is drawn inverse: in the opposite scheme's colors (a dark bar in a light scheme), so that it stands apart from whatever it lands on. False draws it on a surface of the scheme. Null, the default, takes comp.snackbar.container.inverse, the system's choice, which is off in the default system. It changes colors only (SNK-30).
closable boolean false Draws a close button, for a message that stays until the person closes it or for one they may want gone sooner.
closeLabel string "" The close button's accessible name, in the product's language ("Close"). Required while closable is true.
expanded boolean false Whether the whole title and message show. The person toggles it with the expand button (SNK-24); it has no effect while the text fits within its collapsed limits.
expandLabel string "" The expand button's accessible name, in the product's language ("Show full message"). It stays the same while expanded; the button's expanded state says which way it acts. Required whenever the text can be longer than its collapsed limit.
duration auto | short | long | indefinite "auto" How long the snackbar stays by itself. Auto is short without an action and indefinite with one (WCAG 2.2.1: a person must not be timed out of an action). Short and long close after the snackbar's timeout tokens; indefinite stays until it is closed.
placement bottom | top-end "bottom" Where the host places the snackbar in its area. Bottom, the default, is the bottom edge, centred (SNK-13). Top-end is the top edge at the end side: the top right in a left-to-right layout and the top left in a right-to-left one, and across the top of a compact window (SNK-35). A host may be given one placement for all the snackbars it shows; a snackbar that names its own keeps it. It changes where the snackbar stands and the side it enters from (SNK-36, SNK-37), and nothing else: not its size, its form, its order in the queue, its place in the focus order or its announcement.

事件

事件 说明
action The action was activated. The snackbar closes.
dismiss The snackbar closed without its action; carries the reason: close (the close button), escape, timeout, or host (the product closed it or cleared the queue).
toggle The person expanded or collapsed the text with the expand button; carries the new expanded value. Not emitted when the product sets expanded.

状态

行为

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

ID 需求
SNK-1 WHEN the user activates the action by pointer, touch, Enter or Space THE snackbar SHALL emit action exactly once and close; WHEN the user activates the close button the same way THE snackbar SHALL emit dismiss with the reason close exactly once and close.
SNK-2 WHILE the action is hovered, keyboard-focused or pressed THE snackbar SHALL render the action's container, content color and opacity from the component tokens of the highest-precedence active state of the action, in the snackbar's form (SNK-21, SNK-30), with precedence pressed over focused over hovered; focus gained by pointer or touch SHALL NOT select the focused state. The expand and close buttons answer their states as any Button does (SNK-34).
SNK-3 WHEN the action receives keyboard focus THE snackbar SHALL show the focus ring around it without animation; focus gained by pointer or touch SHALL NOT show it. THE snackbar SHALL draw the outer stroke of that indicator, of the indicator inside the text while it scrolls and has keyboard focus (SNK-25) and, through the tone it gives them (SNK-34), of the indicators of the expand and close buttons, in its form's focus-ring color: comp.snackbar.focus-ring.color in the default form, and the token of that name under the form's prefix in every other (comp.snackbar.info.focus-ring.color, comp.snackbar.inverse.focus-ring.color, comp.snackbar.inverse-info.focus-ring.color, and warning's, error's and success's of the same names), never the system's focus-ring color role read directly. The widths and the offset SHALL stay the system's (sys.focus.outer-width, sys.focus.inner-width, sys.focus.offset), as SHALL the color of an inner stroke where the system draws one; under the glow focus style (SNK-7) the form's color SHALL be the halo's outer color.
SNK-4 WHERE the visual size of the action is below the minimum target THE snackbar SHALL extend the action's hit area on both axes to sys.target.touch for touch input and sys.target.pointer for fine pointers, without changing its visual size; a platform that cannot tell the input type SHALL use sys.target.touch.
SNK-5 WHEN the action is pressed by pointer, touch, Enter or Space THE snackbar SHALL show the pressed state without delay and SHALL keep it visible for at least sys.motion.duration.press-hold, even when the press ends sooner.
SNK-6 WHILE the action is pressed THE snackbar SHALL answer as sys.behavior.pressed-feedback names: overlay applies the pressed tokens to the whole container of the action at once; ripple grows the pressed state layer from the point of contact (from the center for keyboard activation); dim applies the pressed opacity to the whole action and draws no state layer; none draws no answer to the press. Under ripple and dim only the container color stays with the state under the press; content color, opacity and shape still come from the pressed tokens. Under none every part stays with the state under the press. The ripple grows over comp.snackbar.motion.feedback-duration with comp.snackbar.motion.state-easing, fades over comp.snackbar.motion.state-duration with comp.snackbar.motion.state-easing once the press has ended and press-hold has passed, and appears whole where motion is reduced.
SNK-7 THE snackbar SHALL ease its container corners and the action's corners by sys.shape.corner-smoothing (0 is a quarter circle, 0.6 the iOS continuous corner; a platform with only the OS curve draws it for any amount above 0) and show its keyboard focus as sys.behavior.focus-style names (ring or glow); a platform that cannot draw a choice SHALL fall back to the first value and state that in its README.
SNK-8 WHILE duration is auto THE snackbar SHALL behave as short when action is empty and as indefinite when action is set. WHILE duration is short or long THE snackbar SHALL close by itself once it has been shown for comp.snackbar.timeout.short or comp.snackbar.timeout.long, and emit dismiss with the reason timeout; WHERE the platform recommends a longer time for the person's accessibility settings (Android's accessibility timeout) it SHALL use that time, and WHERE the platform reports that a screen reader is running a snackbar with an action SHALL NOT close by itself. WHILE duration is indefinite THE snackbar SHALL NOT close by itself.
SNK-9 WHILE the pointer is over the snackbar, focus is inside it, it is expanded, the app's window is hidden or in the background, or a Tanrei modal dialog covers its host THE snackbar SHALL pause its timeout, and WHEN none of these holds any longer SHALL start the timeout again from its whole length.
SNK-10 WHEN a snackbar is shown THE snackbar SHALL have assistive technology announce it once: statusLabel first when status is not default, then the title when it has one, then the whole message (never only the part a collapsed snackbar shows), then the action's label when it has one. It SHALL announce politely (after what is being read) when status is default, info or success, and assertively (interrupting what is being read) when status is warning or error, and SHALL NOT move keyboard or screen-reader focus.
SNK-11 THE snackbar's controls SHALL be reachable in the focus order at the place its host has in that order, in this order whatever row each is drawn on: the text while it scrolls (SNK-25), the action, the expand button, the close button; WHEN the user presses Escape while focus is inside the snackbar THE snackbar SHALL emit dismiss with the reason escape and close; WHEN a snackbar closes while focus is inside it, focus SHALL return to the element that had it before focus entered the snackbar or, where that element no longer exists, to the platform's default.
SNK-12 THE host SHALL show one snackbar at a time, in the order they were requested: WHEN a snackbar is requested while another is showing or closing it SHALL wait until the other has closed, and WHILE a Tanrei modal dialog covers the host a snackbar not yet shown SHALL wait until the dialog closes. Each request SHALL resolve once its snackbar has closed, with action or with dismiss and its reason. WHEN the product closes the showing snackbar THE host SHALL close it and resolve it with dismiss and the reason host, and the next waiting one SHALL then show; WHEN the product clears the queue THE host SHALL also drop every waiting one and resolve each with dismiss and the reason host.
SNK-13 WHILE placement is bottom THE host SHALL place the snackbar at the bottom of its area, centred, comp.snackbar.container.margin above the bottom edge and at least comp.snackbar.container.margin from the side edges, inside the safe area and above the on-screen keyboard; at either placement it SHALL draw the snackbar above its area's content and below a Tanrei modal dialog and its scrim.
SNK-14 THE snackbar SHALL be at least comp.snackbar.container.min-height tall and as wide as its content between comp.snackbar.container.min-width and comp.snackbar.container.max-width, but never wider than its host less comp.snackbar.container.margin on each side. It SHALL draw comp.snackbar.container.color, or its form's container color (SNK-21, SNK-30), with comp.snackbar.container.elevation and comp.snackbar.container.shape corners, and pad its content by comp.snackbar.container.padding-inline-start before the status glyph or, without one, before the text, by comp.snackbar.container.padding-block below the text and by at least as much above it, where SNK-29 places the first line, and after the text by comp.snackbar.container.padding-inline-end when there is no control or by comp.snackbar.container.padding-inline-end-with-controls after the last control. WHILE the action stands on its own row (SNK-16) and no control follows the text on its row, the text SHALL end comp.snackbar.container.padding-inline-end from the end edge, as in a snackbar without controls, and padding-inline-end-with-controls SHALL be the action row's alone.
SNK-15 THE snackbar SHALL draw the message in comp.snackbar.supporting-text.typescale and comp.snackbar.supporting-text.color, or its form's message color (SNK-21, SNK-30), at the opacity SNK-32 gives a message under a title, followed comp.snackbar.container.gap after the text (the title and the message) by the action, the expand button and the close button, in that order, comp.snackbar.controls.gap apart and centred on the text's first line (SNK-29). The action SHALL be drawn as a tonal button, in its form's action colors: a container of comp.snackbar.action.container.color at least comp.snackbar.controls.height tall (the height of a small Button, so that the action stands as tall as the expand and close buttons in its row), the tonal layer over it (SNK-33), and comp.snackbar.controls.padding-inline around its label in comp.snackbar.controls.label.typescale and comp.snackbar.action.content.color, on comp.snackbar.controls.shape corners. The expand and close buttons SHALL be drawn as SNK-34 says.
SNK-16 WHEN the text (the title and the message) would need more than two lines beside the controls, or the action's label would itself wrap, THE snackbar SHALL move the action onto its own row below the text, comp.snackbar.container.gap below it and aligned to the end, with comp.snackbar.container.padding-inline-end-with-controls between that row and the bar's bottom edge, while the expand and close buttons stay at the end of the first row; the text SHALL wrap, and is cut only as SNK-23 says, and the action's label SHALL never wrap or be truncated.
SNK-17 WHEN a snackbar is shown THE snackbar SHALL rise into place from below the bottom edge of its host by its own height plus its margin while fading in (WHILE placement is bottom; at top-end it descends instead, SNK-36), over comp.snackbar.motion.enter-duration with comp.snackbar.motion.enter-easing; WHEN it closes it SHALL fade out in place over comp.snackbar.motion.exit-duration with comp.snackbar.motion.exit-easing, and the host SHALL show the next one only after that. WHERE motion is reduced it SHALL fade in and out without moving, or appear and disappear at once on a platform whose reduced-motion setting removes every animation (Android's).
SNK-18 WHEN the layout direction is right-to-left THE snackbar SHALL mirror its layout (the status glyph and the text at the right, the controls at the left) without mirroring any glyph, and SHALL grow a ripple from the point of contact as in left-to-right.
SNK-19 WHILE a snackbar is drawn outside a host (a snapshot harness, a documentation page) THE snackbar SHALL draw in place at its resting position, centred across the width it is given as SNK-13 centres it in a host (or, WHILE placement is top-end, at the end of that width as SNK-35 places it, and across the whole of it where that width is narrower than comp.snackbar.breakpoint.medium) and at most as tall as the height it is given, the width and the height it is given standing for the host's area in SNK-25 and SNK-35; it SHALL NOT time out, queue, animate its entrance or announce itself, and SHALL still emit action, dismiss and toggle.
SNK-20 WHILE message is empty THE snackbar SHALL still render and in debug builds report the empty message; WHILE closable is true and closeLabel is empty it SHALL still render the close button and in debug builds report that the button has no accessible name; WHILE status is not default and statusLabel is empty it SHALL still render the status form and in debug builds report that the status is not given in words; WHILE the expand button is drawn and expandLabel is empty it SHALL still render it and in debug builds report that it has no accessible name; WHEN duration is short or long and action is set it SHALL, in debug builds, remind the product that a timed action must also be offered elsewhere.
SNK-21 WHILE status is info, warning, error or success THE snackbar SHALL draw that status form: the bar, the title, the message and the status glyph in the status's container, title, supporting-text and icon colors, and every state of the action from the parts prefixed with the status, and give the expand and close buttons the status's tone (SNK-34). For info, while the snackbar is not drawn inverse (SNK-30), these are comp.snackbar.info.container.color, comp.snackbar.info.title.color, comp.snackbar.info.supporting-text.color, comp.snackbar.info.icon.color, comp.snackbar.info.outline.color for its edge (SNK-31), comp.snackbar.action.info.container.color and comp.snackbar.action.info.content.color; warning, error and success read their own tokens of the same names, and a status form drawn inverse reads the tokens SNK-30 names. Before the text it SHALL draw the status's glyph from the system's icon set (info for info, warning for warning, error for error, check_circle for success) at comp.snackbar.icon.size, in its form's icon color drawn at comp.snackbar.icon.opacity (an opacity of the glyph alone, which fades nothing else), comp.snackbar.icon.gap before the text and centred on the text's first line (SNK-29), with the family and fill of an unselected icon, sys.icon.weight and an optical size clamped between sys.icon.optical-size.min and sys.icon.optical-size.max, never mirrored. The glyph so drawn SHALL keep 3:1 against its bar in every scheme of the default system. The glyph SHALL be hidden from assistive technology, and statusLabel SHALL be exposed as text before the title and the message. WHILE status is default THE snackbar SHALL draw no glyph and read the unprefixed tokens, or drawn inverse the tokens prefixed with inverse (SNK-30).
SNK-22 WHILE title is not empty THE snackbar SHALL draw it above the message in comp.snackbar.title.typescale and comp.snackbar.title.color, or its form's title color (SNK-21, SNK-30), with comp.snackbar.title.gap between the title and the message; WHILE title is empty it SHALL draw the message alone. THE snackbar 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.
SNK-23 WHILE expanded is false THE snackbar SHALL show at most comp.snackbar.title.max-lines lines of the title and at most comp.snackbar.supporting-text.max-lines lines of the message, each ending with an ellipsis where it is cut, at every text size, and the whole of both SHALL still reach assistive technology. WHILE the title or the message is longer than its collapsed limit THE snackbar SHALL draw the expand button, holding expand_more while collapsed and expand_less while expanded; WHILE both fit within their limits it SHALL NOT draw the expand button, and expanded SHALL have no effect. WHILE expanded is true THE snackbar SHALL show the whole title and message.
SNK-24 WHEN the user activates the expand button by pointer, touch, Enter or Space THE snackbar SHALL toggle expanded, emit toggle with the new value exactly once, keep focus on the expand button and stay open; the bar SHALL grow to its expanded height or shrink back over comp.snackbar.motion.expand-duration with comp.snackbar.motion.expand-easing, keeping its bottom edge in place WHILE placement is bottom and its top edge at top-end (SNK-37), and change at once where motion is reduced. The expand button SHALL expose to assistive technology whether the text is expanded and which text it shows and hides, and SHALL keep expandLabel as its name in both states. WHEN the product sets expanded THE snackbar SHALL change the same way without emitting toggle.
SNK-25 WHILE the snackbar is expanded and its whole content would make it taller than its host's area allows (the area's height less comp.snackbar.container.margin above and below, inside the safe area and above the on-screen keyboard) THE snackbar SHALL take that height and scroll its text inside the bar, keeping the status glyph, the controls and an action on its own row in place; WHILE the text scrolls it SHALL be reachable by keyboard (a focus stop, scrolled by the arrow keys, Page Up and Page Down), named by the title when there is one, and scrolled by wheel, trackpad and touch.
SNK-26 WHILE the system's icon set cannot yet draw a glyph of the snackbar (its font is still loading) — the status glyph, or the glyph it gives the expand or the close button — THE snackbar SHALL draw nothing in that glyph's place, never the glyph's name as text, SHALL keep the glyph's box, and SHALL draw the glyph once the set can; statusLabel, expandLabel and closeLabel SHALL still reach assistive technology.
SNK-27 THE snackbar 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 snackbar is shown THE snackbar SHALL draw itself for the new direction at once, without a motion of its own, keeping its open state, the time left before it closes and its focus.
SNK-28 THE snackbar SHALL lay out its title, its message and its action's label as text of its own direction, isolated from the text around it: each takes the base direction of its first strong character, or the layout direction where it has none, so that a left-to-right word, a number with its sign or unit, a telephone number and an address keep their own order in a right-to-left layout and reorder nothing beside them; THE snackbar SHALL still place each and align its lines by the layout direction, not by the text's own, and SHALL cut a title or a message cut by a line limit that does not fit (SNK-23) at the end of its own direction, so that a left-to-right label in a right-to-left layout loses its right end and never its beginning.
SNK-29 THE snackbar SHALL align its content to the top of the bar. It SHALL centre the first line of the text (the title's, or the message's without a title) half of comp.snackbar.container.min-height below the bar's top edge, or lower where comp.snackbar.container.padding-block plus half that line's height is more (enlarged text), and SHALL centre the status glyph and every control of the first row (the action while it is beside the text, the expand button and the close button) on that line; every further line of the text follows below it, an action on its own row (SNK-16) follows the text, and any height the bar has beyond its content SHALL lie below the content, never above it. WHEN the bar expands or collapses (SNK-24), and WHEN its text is cut or shown whole (SNK-23), the status glyph, the first line of the title and of the message, and every control of the first row SHALL keep their distance from the bar's top edge, during the motion and after it; an action on its own row SHALL keep its distance from the bar's bottom edge.
SNK-30 THE snackbar SHALL be drawn inverse WHILE inverse is true, or WHILE inverse is null and comp.snackbar.container.inverse is on; otherwise (inverse false, or null where that choice is off) it SHALL be drawn on a surface of the scheme, from the unprefixed tokens or its status form's (SNK-21). WHILE it is drawn inverse and status is default THE snackbar SHALL draw the bar, the title and the message in comp.snackbar.inverse.container.color, comp.snackbar.inverse.title.color and comp.snackbar.inverse.supporting-text.color, and every state of the action from the parts prefixed with inverse (comp.snackbar.action.inverse.container.color and comp.snackbar.action.inverse.content.color), and give the expand and close buttons that form's tone (SNK-34). WHILE it is drawn inverse and status is info, warning, error or success it SHALL draw that status's inverse form the same way, from the parts prefixed with inverse- and the status: for info comp.snackbar.inverse-info.container.color, comp.snackbar.inverse-info.title.color, comp.snackbar.inverse-info.supporting-text.color, comp.snackbar.inverse-info.icon.color, comp.snackbar.inverse-info.outline.color for its edge (SNK-31), comp.snackbar.action.inverse-info.container.color and comp.snackbar.action.inverse-info.content.color, and for warning, error and success their own tokens of the same names. Under the default system each color of a status's inverse form SHALL be the color the same part of that status's form not drawn inverse has in the scheme of the other theme at the same contrast level (light for dark, light-medium for dark-medium, light-high for dark-high, and each the other way): the bar, the title, the message, the glyph, the edge, the focus indicator, every state of the action and the tone of the expand and close buttons, so that the inverse form of a status in one theme and its form on a surface in the other differ only in what lies around the bar. Being drawn inverse SHALL change colors only: the bar's elevation, corners, sizes and layout, the glyphs, the width and the opacity of a status form's edge, the opacities of the glyph, of a message under a title and of the action's tonal layer, the motion, the timing, the announcement and the keyboard behaviour SHALL be those of the same snackbar not drawn inverse. WHEN inverse or the system's choice changes while the snackbar is shown THE snackbar SHALL draw the other form at once, keeping its open state, the time left before it closes and its focus.
SNK-31 WHILE status is info, warning, error or success THE snackbar SHALL draw its form's edge: a line comp.snackbar.outline.width wide around the bar, outside the bar's bounds and following its corners, where the ring of comp.snackbar.container.elevation lies (the layer of that elevation with no offset, no blur and a positive spread), over that ring and above the elevation's shadows, in the form's outline color drawn at comp.snackbar.outline.opacity: comp.snackbar.info.outline.color for info, comp.snackbar.inverse-info.outline.color for info drawn inverse (SNK-30), and warning's, error's and success's tokens of the same names. The edge SHALL NOT change the bar's size or the place of anything inside or around it, and the focus indicator of a control SHALL be drawn above it. WHILE status is default THE snackbar SHALL draw no edge of its own, drawn inverse or not: the ring of its elevation shows alone, as on every raised surface. WHERE comp.snackbar.outline.opacity is 0 a status form SHALL draw no edge either.
SNK-32 WHILE title is not empty THE snackbar SHALL draw the message at comp.snackbar.supporting-text.under-title.opacity: an opacity of the message's text alone, applied to its form's message color (SNK-15), in every form, collapsed and expanded. WHILE title is empty it SHALL draw the message whole, and it SHALL draw the title whole in every form. The message so drawn SHALL keep 4.5:1 against its bar in every scheme of the default system, and 7:1 in its high-contrast schemes; what assistive technology reads SHALL NOT change with it.
SNK-33 THE snackbar SHALL draw the action's tonal layer in every form and in every state of the action: the action's content color of that form and state, at comp.snackbar.action.tonal-layer.opacity, filling the action's container to its corners, above the container color of that state (SNK-2) and above a ripple that grows on it (SNK-6), and under the label. The layer is the action's alone, SHALL NOT take a press or change the action's size, and SHALL NOT be drawn WHERE comp.snackbar.action.tonal-layer.opacity is 0, where the action is its container alone. The label SHALL keep 4.5:1 against the action as drawn (the container color of the state with the layer over it) in every form, state and scheme of the default system, and 7:1 in its high-contrast schemes.
SNK-34 WHILE closable is true, and WHILE the expand button is drawn (SNK-23), THE snackbar SHALL draw that button, in the place SNK-15, SNK-16 and SNK-29 give it, as an icon-only Button of the small size showing a glyph of the system's icon set (close; expand_more or expand_less, SNK-23) and named by closeLabel or expandLabel, in the variant comp.snackbar.close-button.variant names and in the tone of the snackbar's form (BTN-45): the form's message color — comp.snackbar.supporting-text.color in the default form, and the token of that name under the form's prefix in every other (SNK-21, SNK-30) — with the form's focus-ring color (SNK-3) for its focus indicator, both given through the channel a Button reads from the component that places it, and otherwise unrestyled: it reads Button's own tokens for its size, glyph size, corners and state opacities, answers hover, focus and press as any Button does, and keeps its own minimum target (BTN-5). WHEN the snackbar's form changes while it is shown THE snackbar SHALL give the buttons the new form's tone with the new form's colors (SNK-30). THE snackbar SHALL draw no close or expand mark of its own (no path, shape or character of its own), and the expand button SHALL expose to assistive technology whether the text is expanded and which text it shows and hides (SNK-24).
SNK-35 WHILE placement is top-end THE host SHALL place the snackbar at the top of its area, at its end side (the right in a left-to-right layout and the left in a right-to-left one, taken from where the host is used as SNK-27 says), comp.snackbar.container.margin below the top edge and comp.snackbar.container.margin from the end edge, inside the safe area and below what the frame keeps over the top of the area (a sticky header's present height, APS-17), so that it never covers a top app bar; WHILE the area is narrower than comp.snackbar.breakpoint.medium (a compact window) it SHALL draw the snackbar across the top of the area instead, as wide as the area less comp.snackbar.container.margin on each side, within comp.snackbar.container.max-width. WHEN the product asks for a snackbar without a placement THE host SHALL give it the placement the host itself is given, and bottom where it is given none. The placement SHALL NOT change the snackbar's layer (SNK-13), its other size bounds (SNK-14), its place in the focus order (SNK-11) or its announcement (SNK-10), and the host SHALL still show one snackbar at a time, in order (SNK-12): at the top as at the bottom, a snackbar asked for while another shows waits for it to close, and no two are stacked.
SNK-36 WHILE placement is top-end, WHEN a snackbar is shown THE snackbar SHALL descend into place from above the top edge of its host by its own height plus its margin while fading in, over comp.snackbar.motion.enter-duration with comp.snackbar.motion.enter-easing, as SNK-17 rises at the bottom; WHEN it closes it SHALL fade out in place as SNK-17 says, and WHERE motion is reduced it SHALL fade in and out without moving, or appear and disappear at once where the platform removes every animation.
SNK-37 WHILE placement is top-end, WHEN the bar expands or collapses (SNK-24) THE snackbar SHALL grow downward and shrink back with its top edge in place, so that, its content being aligned to the top (SNK-29), nothing of its first row moves on the screen during the motion or after it; WHILE it is expanded it SHALL be at most as tall as SNK-25 allows, the area's height being measured from below what the frame keeps over its top (SNK-35).

各平台名称对照

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

规格 Web SwiftUI Jetpack Compose Flutter React Native
组件 tanrei-snackbar TanreiSnackbar TanreiSnackbar TanreiSnackbar Snackbar
message · 属性 message message message message message
title · 属性 headline title title title title
action · 属性 action actionLabel actionLabel actionLabel actionLabel
status · 属性 status status status status status
statusLabel · 属性 statusLabel statusLabel statusLabel statusLabel statusLabel
inverse · 属性 inverse inverse inverse inverse inverse
closable · 属性 closable withCloseButton withDismissAction showCloseIcon closable
closeLabel · 属性 closeLabel closeLabel closeLabel closeIconLabel closeLabel
expanded · 属性 expanded isExpanded expanded expanded expanded
expandLabel · 属性 expandLabel expandLabel expandLabel expandLabel expandLabel
duration · 属性 duration duration duration duration duration
placement · 属性 placement placement placement placement placement
action · 事件 action TanreiSnackbarResult.action TanreiSnackbarResult.ActionPerformed TanreiSnackbarClosedReason.action onAction
dismiss · 事件 dismiss TanreiSnackbarResult.dismissed TanreiSnackbarResult.Dismissed TanreiSnackbarClosedReason onDismiss
toggle · 事件 expandedchange onExpandedChange onExpandedChange onExpandedChanged onExpandedChange
备注 title is the headline property and attribute, because HTML's global title attribute would show a tooltip over the whole bar (as the app bar's). closeLabel, statusLabel and expandLabel are the close-label, status-label and expand-label attributes. inverse is a property that is true, false or null (the default); the inverse attribute sets it: absent is null, present (empty or true) is true, and inverse="false" is false, the one way to refuse a system whose choice is on. While it is null the snackbar reads --tanrei-comp-snackbar-container-inverse from the cascade, which carries the choice's name (SNK-30). The opacities of SNK-21, SNK-31, SNK-32 and SNK-33 are applied to the color of the part they name (a mix of that color with transparent), never as an element's opacity, so nothing else fades with it; a status form's edge is a shadow layer of its own before the elevation's, and the tonal layer lies inside the action, under its label. The focus indicator's color is the form's focus-ring-color custom property, handed to the shared focus primitive (SNK-3). The expand and close buttons are <tanrei-button size="small"> elements in the snackbar's shadow root, given the variant --tanrei-comp-snackbar-close-button-variant names and the form's tone through the Button's internal channel (SNK-34); the expand button's expanded state and the text it controls are written on the Button's own native button (aria-expanded, and ariaControlsElements, since an id cannot be named across the Button's shadow root). dismiss is a CustomEvent whose detail.reason is the reason. toggle is the expandedchange CustomEvent, whose detail.expanded is the new value, because a popover element already fires a native toggle when it is shown and hidden. The host (<tanrei-snackbar-host>) queues and places snackbars; show(options) takes title, status, statusLabel, inverse and expandLabel as well; see the accessibility notes. placement is the placement attribute of <tanrei-snackbar> and of <tanrei-snackbar-host>: the host's is the placement of each snackbar whose show() options name none (SNK-35). What the frame keeps over the top of the area is --tanrei-app-shell-scroll-offset on the document's root, which <tanrei-app-shell> sets to its sticky header's height (APS-17) and a product's own frame may set; the safe area is env(safe-area-inset-*). A compact window is one narrower than --tanrei-comp-snackbar-breakpoint-medium, read by the stylesheet itself, with no listener. Snapshot harness: each fixture is a <tanrei-snackbar> rendered outside a host (SNK-19), on a 400 px wide stage; expanded-scrolls on a stage 400 × 240, and top-end and top-end-rtl on a stage 720 wide, wider than a compact window. Lagging: ships an earlier form of the snackbar: always an inverse bar, with a text action, no status forms, no title, no expansion and no inverse. A host is a @Observable TanreiSnackbarHost attached with .tanreiSnackbarHost(_:) on the view whose bottom edge it uses; await host.show(message:actionLabel:withCloseButton:closeLabel:duration:) returns a TanreiSnackbarResult (.action, or .dismissed(reason)) once the snackbar has closed. It is drawn in an overlay of that view, not in a sheet, which would be modal. Snapshot harness: TanreiSnackbar rendered in place (SNK-19) in a 400-point frame. inverse is a Bool?, nil by default. Not yet: the top-end placement (SNK-35, SNK-36, SNK-37); the expand and close buttons as small Buttons in the form's tone (SNK-34); the title's letter-spacing by the script of its text (SNK-22, PRS-20). Lagging: ships an earlier form of the snackbar: always an inverse bar, with a text action, no status forms, no title, no expansion and no inverse. Shaped like Material's snackbar API: TanreiSnackbarHost(hostState) is placed in the layout (for example a scaffold's snackbar slot), and hostState.showSnackbar(message, actionLabel, withDismissAction, closeLabel, duration) suspends until the snackbar has closed and returns the result; cancelling the coroutine closes it with the reason host. The recommended timeout is AccessibilityManager.calculateRecommendedTimeoutMillis. Snapshot harness: TanreiSnackbar(message = …) rendered in place (SNK-19) in a 400 dp wide box. inverse is a Boolean?, null by default. Not yet: the top-end placement (SNK-35, SNK-36, SNK-37); the expand and close buttons as small Buttons in the form's tone (SNK-34); the title's letter-spacing by the script of its text (SNK-22, PRS-20). Lagging: ships an earlier form of the snackbar: always an inverse bar, with a text action, no status forms, no title, no expansion and no inverse. Shaped like Flutter's ScaffoldMessenger: a TanreiSnackbarMessenger placed around the page, inside its route, hosts the queue (so a Tanrei dialog's route covers it and SNK-12's wait applies), and TanreiSnackbarMessenger.of(context).show(TanreiSnackbar(...)) returns a controller whose closed future completes with a TanreiSnackbarClosedReason (action, close, escape, timeout, host). showCloseIcon is Flutter's own name for the close button. A running screen reader is MediaQuery.accessibleNavigation (as Flutter's own SnackBar reads it); Android's recommended timeout needs a platform channel, since Flutter does not expose it. Snapshot harness: a golden of TanreiSnackbar rendered in place (SNK-19) in a 400-wide surface. inverse is a bool?, null by default. Not yet: the top-end placement (SNK-35, SNK-36, SNK-37); the expand and close buttons as small Buttons in the form's tone (SNK-34); the title's letter-spacing by the script of its text (SNK-22, PRS-20). Lagging: ships an earlier form of the snackbar: always an inverse bar, with a text action, no status forms, no title, no expansion and no inverse. TanreiProvider renders the host in its root overlay layer, not in a Modal (a Modal is modal and takes focus); useSnackbar().show({ message, actionLabel, closable, closeLabel, duration }) returns a promise of the result. A Snackbar element rendered directly is drawn in place (SNK-19). Snapshot harness: Snackbar rendered in place in a 400-wide view. inverse is boolean | null | undefined; both null and undefined take the system's choice. Not yet: the top-end placement (SNK-35, SNK-36, SNK-37); the expand and close buttons as small Buttons in the form's tone (SNK-34); the title's letter-spacing by the script of its text (SNK-22, PRS-20).

令牌

令牌 引用 解析值(浅色)
--tanrei-comp-snackbar-action-container-color comp.snackbar.container.color #ffffff
--tanrei-comp-snackbar-action-content-color comp.snackbar.supporting-text.color #1b1b1b
--tanrei-comp-snackbar-action-error-container-color comp.snackbar.error.container.color #ffedeb
--tanrei-comp-snackbar-action-error-content-color comp.snackbar.error.supporting-text.color #3a211f
--tanrei-comp-snackbar-action-error-state-layer-color comp.snackbar.action.error.content.color #3a211f
--tanrei-comp-snackbar-action-focused-container-color — #e8e8e8
--tanrei-comp-snackbar-action-focused-content-color — #1b1b1b
--tanrei-comp-snackbar-action-focused-error-container-color — #ebd9d7
--tanrei-comp-snackbar-action-focused-error-content-color — #3a211f
--tanrei-comp-snackbar-action-focused-error-state-layer-color — #3a211f
--tanrei-comp-snackbar-action-focused-info-container-color — #d9dceb
--tanrei-comp-snackbar-action-focused-info-content-color — #212839
--tanrei-comp-snackbar-action-focused-info-state-layer-color — #212839
--tanrei-comp-snackbar-action-focused-inverse-error-container-color — #4a312f
--tanrei-comp-snackbar-action-focused-inverse-error-content-color — #ffe1df
--tanrei-comp-snackbar-action-focused-inverse-error-state-layer-color — #ffe1df
--tanrei-comp-snackbar-action-focused-inverse-info-container-color — #303748
--tanrei-comp-snackbar-action-focused-inverse-info-content-color — #e1e8ff
--tanrei-comp-snackbar-action-focused-inverse-info-state-layer-color — #e1e8ff
--tanrei-comp-snackbar-action-focused-inverse-success-container-color — #2b3b2b
--tanrei-comp-snackbar-action-focused-inverse-success-content-color — #daeed6
--tanrei-comp-snackbar-action-focused-inverse-success-state-layer-color — #daeed6
--tanrei-comp-snackbar-action-focused-inverse-warning-container-color — #47341f
--tanrei-comp-snackbar-action-focused-inverse-warning-content-color — #ffe3cb
--tanrei-comp-snackbar-action-focused-inverse-warning-state-layer-color — #ffe3cb
--tanrei-comp-snackbar-action-focused-inverse-container-color — #343434
--tanrei-comp-snackbar-action-focused-inverse-content-color — #f1f1f1
--tanrei-comp-snackbar-action-focused-inverse-state-layer-color — #f1f1f1
--tanrei-comp-snackbar-action-focused-state-layer-color — #1b1b1b
--tanrei-comp-snackbar-action-focused-success-container-color — #cee2cc
--tanrei-comp-snackbar-action-focused-success-content-color — #1b2c1c
--tanrei-comp-snackbar-action-focused-success-state-layer-color — #1b2c1c
--tanrei-comp-snackbar-action-focused-warning-container-color — #ebdacb
--tanrei-comp-snackbar-action-focused-warning-content-color — #362410
--tanrei-comp-snackbar-action-focused-warning-state-layer-color — #362410
--tanrei-comp-snackbar-action-hovered-container-color — #ededed
--tanrei-comp-snackbar-action-hovered-content-color — #1b1b1b
--tanrei-comp-snackbar-action-hovered-error-container-color — #efdddb
--tanrei-comp-snackbar-action-hovered-error-content-color — #3a211f
--tanrei-comp-snackbar-action-hovered-error-state-layer-color — #3a211f
--tanrei-comp-snackbar-action-hovered-info-container-color — #dde0ef
--tanrei-comp-snackbar-action-hovered-info-content-color — #212839
--tanrei-comp-snackbar-action-hovered-info-state-layer-color — #212839
--tanrei-comp-snackbar-action-hovered-inverse-error-container-color — #462d2b
--tanrei-comp-snackbar-action-hovered-inverse-error-content-color — #ffe1df
--tanrei-comp-snackbar-action-hovered-inverse-error-state-layer-color — #ffe1df
--tanrei-comp-snackbar-action-hovered-inverse-info-container-color — #2c3344
--tanrei-comp-snackbar-action-hovered-inverse-info-content-color — #e1e8ff
--tanrei-comp-snackbar-action-hovered-inverse-info-state-layer-color — #e1e8ff
--tanrei-comp-snackbar-action-hovered-inverse-success-container-color — #273727
--tanrei-comp-snackbar-action-hovered-inverse-success-content-color — #daeed6
--tanrei-comp-snackbar-action-hovered-inverse-success-state-layer-color — #daeed6
--tanrei-comp-snackbar-action-hovered-inverse-warning-container-color — #42301b
--tanrei-comp-snackbar-action-hovered-inverse-warning-content-color — #ffe3cb
--tanrei-comp-snackbar-action-hovered-inverse-warning-state-layer-color — #ffe3cb
--tanrei-comp-snackbar-action-hovered-inverse-container-color — #303030
--tanrei-comp-snackbar-action-hovered-inverse-content-color — #f1f1f1
--tanrei-comp-snackbar-action-hovered-inverse-state-layer-color — #f1f1f1
--tanrei-comp-snackbar-action-hovered-state-layer-color — #1b1b1b
--tanrei-comp-snackbar-action-hovered-success-container-color — #d2e6cf
--tanrei-comp-snackbar-action-hovered-success-content-color — #1b2c1c
--tanrei-comp-snackbar-action-hovered-success-state-layer-color — #1b2c1c
--tanrei-comp-snackbar-action-hovered-warning-container-color — #efdecf
--tanrei-comp-snackbar-action-hovered-warning-content-color — #362410
--tanrei-comp-snackbar-action-hovered-warning-state-layer-color — #362410
--tanrei-comp-snackbar-action-info-container-color comp.snackbar.info.container.color #edf0ff
--tanrei-comp-snackbar-action-info-content-color comp.snackbar.info.supporting-text.color #212839
--tanrei-comp-snackbar-action-info-state-layer-color comp.snackbar.action.info.content.color #212839
--tanrei-comp-snackbar-action-inverse-error-container-color comp.snackbar.inverse-error.container.color #361d1b
--tanrei-comp-snackbar-action-inverse-error-content-color comp.snackbar.inverse-error.supporting-text.color #ffe1df
--tanrei-comp-snackbar-action-inverse-error-state-layer-color comp.snackbar.action.inverse-error.content.color #ffe1df
--tanrei-comp-snackbar-action-inverse-info-container-color comp.snackbar.inverse-info.container.color #1c2334
--tanrei-comp-snackbar-action-inverse-info-content-color comp.snackbar.inverse-info.supporting-text.color #e1e8ff
--tanrei-comp-snackbar-action-inverse-info-state-layer-color comp.snackbar.action.inverse-info.content.color #e1e8ff
--tanrei-comp-snackbar-action-inverse-success-container-color comp.snackbar.inverse-success.container.color #172718
--tanrei-comp-snackbar-action-inverse-success-content-color comp.snackbar.inverse-success.supporting-text.color #daeed6
--tanrei-comp-snackbar-action-inverse-success-state-layer-color comp.snackbar.action.inverse-success.content.color #daeed6
--tanrei-comp-snackbar-action-inverse-warning-container-color comp.snackbar.inverse-warning.container.color #32200c
--tanrei-comp-snackbar-action-inverse-warning-content-color comp.snackbar.inverse-warning.supporting-text.color #ffe3cb
--tanrei-comp-snackbar-action-inverse-warning-state-layer-color comp.snackbar.action.inverse-warning.content.color #ffe3cb
--tanrei-comp-snackbar-action-inverse-container-color comp.snackbar.inverse.container.color #1f1f1f
--tanrei-comp-snackbar-action-inverse-content-color comp.snackbar.inverse.supporting-text.color #f1f1f1
--tanrei-comp-snackbar-action-inverse-state-layer-color comp.snackbar.action.inverse.content.color #f1f1f1
--tanrei-comp-snackbar-action-pressed-container-color — #e8e8e8
--tanrei-comp-snackbar-action-pressed-content-color — #1b1b1b
--tanrei-comp-snackbar-action-pressed-error-container-color — #ebd9d7
--tanrei-comp-snackbar-action-pressed-error-content-color — #3a211f
--tanrei-comp-snackbar-action-pressed-error-state-layer-color — #3a211f
--tanrei-comp-snackbar-action-pressed-info-container-color — #d9dceb
--tanrei-comp-snackbar-action-pressed-info-content-color — #212839
--tanrei-comp-snackbar-action-pressed-info-state-layer-color — #212839
--tanrei-comp-snackbar-action-pressed-inverse-error-container-color — #4a312f
--tanrei-comp-snackbar-action-pressed-inverse-error-content-color — #ffe1df
--tanrei-comp-snackbar-action-pressed-inverse-error-state-layer-color — #ffe1df
--tanrei-comp-snackbar-action-pressed-inverse-info-container-color — #303748
--tanrei-comp-snackbar-action-pressed-inverse-info-content-color — #e1e8ff
--tanrei-comp-snackbar-action-pressed-inverse-info-state-layer-color — #e1e8ff
--tanrei-comp-snackbar-action-pressed-inverse-success-container-color — #2b3b2b
--tanrei-comp-snackbar-action-pressed-inverse-success-content-color — #daeed6
--tanrei-comp-snackbar-action-pressed-inverse-success-state-layer-color — #daeed6
--tanrei-comp-snackbar-action-pressed-inverse-warning-container-color — #47341f
--tanrei-comp-snackbar-action-pressed-inverse-warning-content-color — #ffe3cb
--tanrei-comp-snackbar-action-pressed-inverse-warning-state-layer-color — #ffe3cb
--tanrei-comp-snackbar-action-pressed-inverse-container-color — #343434
--tanrei-comp-snackbar-action-pressed-inverse-content-color — #f1f1f1
--tanrei-comp-snackbar-action-pressed-inverse-state-layer-color — #f1f1f1
--tanrei-comp-snackbar-action-pressed-state-layer-color — #1b1b1b
--tanrei-comp-snackbar-action-pressed-success-container-color — #cee2cc
--tanrei-comp-snackbar-action-pressed-success-content-color — #1b2c1c
--tanrei-comp-snackbar-action-pressed-success-state-layer-color — #1b2c1c
--tanrei-comp-snackbar-action-pressed-warning-container-color — #ebdacb
--tanrei-comp-snackbar-action-pressed-warning-content-color — #362410
--tanrei-comp-snackbar-action-pressed-warning-state-layer-color — #362410
--tanrei-comp-snackbar-action-state-layer-color comp.snackbar.action.content.color #1b1b1b
--tanrei-comp-snackbar-action-success-container-color comp.snackbar.success.container.color #e2f6df
--tanrei-comp-snackbar-action-success-content-color comp.snackbar.success.supporting-text.color #1b2c1c
--tanrei-comp-snackbar-action-success-state-layer-color comp.snackbar.action.success.content.color #1b2c1c
--tanrei-comp-snackbar-action-tonal-layer-opacity — 0.12
--tanrei-comp-snackbar-action-warning-container-color comp.snackbar.warning.container.color #ffeee0
--tanrei-comp-snackbar-action-warning-content-color comp.snackbar.warning.supporting-text.color #362410
--tanrei-comp-snackbar-action-warning-state-layer-color comp.snackbar.action.warning.content.color #362410
--tanrei-comp-snackbar-breakpoint-medium sys.breakpoint.medium 600px
--tanrei-comp-snackbar-close-button-variant — 1
--tanrei-comp-snackbar-container-color sys.color.surface-bright #ffffff
--tanrei-comp-snackbar-container-elevation sys.elevation.level3 0px 0px 0px 1px rgb(0 0 0 / 0.08), 0px 1px 1px 0px rgb(0 0 0 / 0.04), 0px 4px 8px -4px rgb(0 0 0 / 0.04), 0px 16px 24px -8px rgb(0 0 0 / 0.06)
--tanrei-comp-snackbar-container-gap sys.space.space100 8px
--tanrei-comp-snackbar-container-inverse — 0
--tanrei-comp-snackbar-container-margin sys.space.space200 16px
--tanrei-comp-snackbar-container-max-width — 560px
--tanrei-comp-snackbar-container-min-height — 48px
--tanrei-comp-snackbar-container-min-width — 288px
--tanrei-comp-snackbar-container-padding-block sys.space.space150 12px
--tanrei-comp-snackbar-container-padding-inline-end sys.space.space200 16px
--tanrei-comp-snackbar-container-padding-inline-end-with-controls sys.space.space100 8px
--tanrei-comp-snackbar-container-padding-inline-start sys.space.space200 16px
--tanrei-comp-snackbar-container-shape sys.shape.corner.medium 8px
--tanrei-comp-snackbar-controls-gap sys.space.space50 4px
--tanrei-comp-snackbar-controls-height comp.button.container.height.small 32px
--tanrei-comp-snackbar-controls-padding-inline sys.space.space175 14px
--tanrei-comp-snackbar-controls-shape sys.shape.corner.full 9999px
--tanrei-comp-snackbar-error-container-color sys.color.error-container #ffedeb
--tanrei-comp-snackbar-error-focus-ring-color sys.color.on-error-container #3a211f
--tanrei-comp-snackbar-error-icon-color sys.color.error #cd262f
--tanrei-comp-snackbar-error-outline-color sys.color.error #cd262f
--tanrei-comp-snackbar-error-supporting-text-color sys.color.on-error-container #3a211f
--tanrei-comp-snackbar-error-title-color comp.snackbar.error.supporting-text.color #3a211f
--tanrei-comp-snackbar-focus-ring-color sys.color.focus-ring #0070ac
--tanrei-comp-snackbar-focused-opacity — 1
--tanrei-comp-snackbar-hovered-opacity — 1
--tanrei-comp-snackbar-icon-gap sys.space.space150 12px
--tanrei-comp-snackbar-icon-opacity — 0.85
--tanrei-comp-snackbar-icon-size sys.icon.medium 24px
--tanrei-comp-snackbar-info-container-color sys.color.info-container #edf0ff
--tanrei-comp-snackbar-info-focus-ring-color sys.color.on-info-container #212839
--tanrei-comp-snackbar-info-icon-color sys.color.info #3066cb
--tanrei-comp-snackbar-info-outline-color sys.color.info #3066cb
--tanrei-comp-snackbar-info-supporting-text-color sys.color.on-info-container #212839
--tanrei-comp-snackbar-info-title-color comp.snackbar.info.supporting-text.color #212839
--tanrei-comp-snackbar-inverse-error-container-color sys.color.on-error-container #361d1b
--tanrei-comp-snackbar-inverse-error-focus-ring-color comp.snackbar.inverse-error.supporting-text.color #ffe1df
--tanrei-comp-snackbar-inverse-error-icon-color comp.snackbar.inverse-error.supporting-text.color #ff928c
--tanrei-comp-snackbar-inverse-error-outline-color sys.color.error #ff928c
--tanrei-comp-snackbar-inverse-error-supporting-text-color sys.color.error-container #ffe1df
--tanrei-comp-snackbar-inverse-error-title-color comp.snackbar.inverse-error.supporting-text.color #ffe1df
--tanrei-comp-snackbar-inverse-info-container-color sys.color.on-info-container #1c2334
--tanrei-comp-snackbar-inverse-info-focus-ring-color comp.snackbar.inverse-info.supporting-text.color #e1e8ff
--tanrei-comp-snackbar-inverse-info-icon-color comp.snackbar.inverse-info.supporting-text.color #8eafff
--tanrei-comp-snackbar-inverse-info-outline-color sys.color.info #8eafff
--tanrei-comp-snackbar-inverse-info-supporting-text-color sys.color.info-container #e1e8ff
--tanrei-comp-snackbar-inverse-info-title-color comp.snackbar.inverse-info.supporting-text.color #e1e8ff
--tanrei-comp-snackbar-inverse-success-container-color sys.color.on-success-container #172718
--tanrei-comp-snackbar-inverse-success-focus-ring-color comp.snackbar.inverse-success.supporting-text.color #daeed6
--tanrei-comp-snackbar-inverse-success-icon-color comp.snackbar.inverse-success.supporting-text.color #62c572
--tanrei-comp-snackbar-inverse-success-outline-color sys.color.success #62c572
--tanrei-comp-snackbar-inverse-success-supporting-text-color sys.color.success-container #daeed6
--tanrei-comp-snackbar-inverse-success-title-color comp.snackbar.inverse-success.supporting-text.color #daeed6
--tanrei-comp-snackbar-inverse-warning-container-color sys.color.on-warning-container #32200c
--tanrei-comp-snackbar-inverse-warning-focus-ring-color comp.snackbar.inverse-warning.supporting-text.color #ffe3cb
--tanrei-comp-snackbar-inverse-warning-icon-color comp.snackbar.inverse-warning.supporting-text.color #f39e3a
--tanrei-comp-snackbar-inverse-warning-outline-color sys.color.warning #f39e3a
--tanrei-comp-snackbar-inverse-warning-supporting-text-color sys.color.warning-container #ffe3cb
--tanrei-comp-snackbar-inverse-warning-title-color comp.snackbar.inverse-warning.supporting-text.color #ffe3cb
--tanrei-comp-snackbar-inverse-container-color sys.color.inverse-surface #1f1f1f
--tanrei-comp-snackbar-inverse-focus-ring-color sys.color.inverse-on-surface #f1f1f1
--tanrei-comp-snackbar-inverse-supporting-text-color sys.color.inverse-on-surface #f1f1f1
--tanrei-comp-snackbar-inverse-title-color comp.snackbar.inverse.supporting-text.color #f1f1f1
--tanrei-comp-snackbar-motion-enter-duration sys.motion.role.appear.toast.enter 350ms
--tanrei-comp-snackbar-motion-enter-easing sys.motion.easing.enter cubic-bezier(0.2, 0, 0, 1)
--tanrei-comp-snackbar-motion-exit-duration sys.motion.role.appear.toast.exit 300ms
--tanrei-comp-snackbar-motion-exit-easing sys.motion.easing.exit cubic-bezier(0.3, 0, 1, 1)
--tanrei-comp-snackbar-motion-expand-duration sys.motion.role.resize.large 400ms
--tanrei-comp-snackbar-motion-expand-easing sys.motion.easing.spatial cubic-bezier(0.2, 0, 0, 1)
--tanrei-comp-snackbar-motion-feedback-duration sys.motion.role.feedback 450ms
--tanrei-comp-snackbar-motion-state-duration sys.motion.role.state.default 250ms
--tanrei-comp-snackbar-motion-state-easing sys.motion.easing.state cubic-bezier(0.2, 0, 0, 1)
--tanrei-comp-snackbar-outline-opacity — 0.4
--tanrei-comp-snackbar-outline-width sys.border.hairline 1px
--tanrei-comp-snackbar-pressed-opacity — 1
--tanrei-comp-snackbar-success-container-color sys.color.success-container #e2f6df
--tanrei-comp-snackbar-success-focus-ring-color sys.color.on-success-container #1b2c1c
--tanrei-comp-snackbar-success-icon-color sys.color.success #077b32
--tanrei-comp-snackbar-success-outline-color sys.color.success #077b32
--tanrei-comp-snackbar-success-supporting-text-color sys.color.on-success-container #1b2c1c
--tanrei-comp-snackbar-success-title-color comp.snackbar.success.supporting-text.color #1b2c1c
--tanrei-comp-snackbar-supporting-text-color sys.color.on-surface #1b1b1b
--tanrei-comp-snackbar-supporting-text-max-lines — 3
--tanrei-comp-snackbar-supporting-text-under-title-opacity — 0.8
--tanrei-comp-snackbar-timeout-long — 10000ms
--tanrei-comp-snackbar-timeout-short — 4000ms
--tanrei-comp-snackbar-title-color comp.snackbar.supporting-text.color #1b1b1b
--tanrei-comp-snackbar-title-gap sys.space.space50 4px
--tanrei-comp-snackbar-title-max-lines — 3
--tanrei-comp-snackbar-warning-container-color sys.color.warning-container #ffeee0
--tanrei-comp-snackbar-warning-focus-ring-color sys.color.on-warning-container #362410
--tanrei-comp-snackbar-warning-icon-color sys.color.warning #9b5c00
--tanrei-comp-snackbar-warning-outline-color sys.color.warning #9b5c00
--tanrei-comp-snackbar-warning-supporting-text-color sys.color.on-warning-container #362410
--tanrei-comp-snackbar-warning-title-color comp.snackbar.warning.supporting-text.color #362410
--tanrei-comp-snackbar-controls-label-typescale sys.typescale.label 500 14px/1.4286
--tanrei-comp-snackbar-supporting-text-typescale sys.typescale.body 400 14px/1.5714
--tanrei-comp-snackbar-title-typescale sys.typescale.heading-6 500 14px/1.4286

测试样例

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

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

指南

何时使用

当用户刚做的事已经完成、又没有属于它自己的屏幕时,用消息条来确认:“会话已归档”“链接已复制”“照片已保存”。最多提供一个操作,用来撤销这件事或接着做下一步。

消息条只报告,不提问。在两个操作之间做选择、阻碍用户工作的错误,或任何必须由用户确认知悉的事,都应放在对话框里,或放在与它相关的东西旁边。错误形态用于用户可以回头再处理的失败,例如带“重试”的“消息未发送”。

消息条还是对话框

两者都出现在页面之上,但做的事正好相反。

方面 消息条 对话框
它做什么 报告已经发生的事。 在工作继续之前,请用户做出决定,或确认知悉某件事。
焦点 从不移动。用户继续工作。 移入对话框。它背后的页面无法使用。
停留多久 几秒钟,或直到它的操作或关闭按钮被使用。 直到用户作答。
操作 最多一个,而且没有它消息也是完整的。 对它所提问题的回答。

如果用户忽略这条消息也不会出什么问题,它就是消息条。如果应用没有回答就无法继续,它就是对话框。属于屏幕某一部分、并且必须保持可见的消息(例如某个输入框出了什么问题),应放在那个位置,例如作为该输入框的辅助文本。

部件

部件 设置方式 说明
条 inverse 默认是当前配色方案中一个抬升的表面,也可以采用相反配色方案的颜色;见在表面上,还是反色。
状态图标 status 和 statusLabel 可选。状态形态会在文本之前绘制它的图标;见状态形态。
标题 title(在 Web 上是 headline) 可选。消息上方的简短标题,字号与消息相同,样式更重。没有它时,消息独自成立。
消息 message 必填。纯文本,一两个短句,朗读时总是完整的。有标题时,它是描述,画得比标题略淡一些。
操作 action(标签) 可选:“撤销”“查看”“重试”。绘制为 tonal 按钮:一个胶囊形,与它所在的条只差低调的一级,标签采用条的文本颜色。消息条用自己的令牌来绘制它,而不是把它作为 Button,高度与旁边的 small Button 相同。当旁边的文本需要超过两行(标题和消息的行数合计)时,或它的标签需要换行时,它会移到单独的一行。
展开按钮 expandLabel、expanded 一个纯图标的 small Button,绘制方式与关闭按钮相同。只在标题或消息超过三行时绘制。expandLabel 是它的无障碍名称,例如“显示完整消息”。
关闭按钮 closable 和 closeLabel 可选。一个纯图标的 small Button。用于一直停留到被关闭的消息,或用户可能想让它早点消失的消息。closeLabel 是它的无障碍名称,使用你的产品的语言。

操作、展开按钮和关闭按钮位于文本第一行的旁边,三者都与 small Button 一样高。

展开按钮和关闭按钮

展开按钮和关闭按钮是纯图标的 small Button,使用图标集的 expand_more、expand_less 和 close 图标。和 Tanrei 中所有的关闭按钮一样,它们是 tonal 的,颜色取自消息条:图标是条上文本的颜色,下面铺一个由这种颜色变成半透明的圆。因此在每一种形态上,无论表面、状态的淡色还是反色的条,它们都看得出是控件,也不会带来条上本没有的颜色。它们的悬停、聚焦和按下是 Button 的,叠在同一种颜色上;焦点环是条自己的焦点颜色。

产品也可以只把它们绘制成图标。做法是在设计系统层里,把 comp.snackbar.close-button.variant 设为 text 变体,一次作用于所有消息条。Material 和 Cupertino 预设正是这样做的,以保留原设计中只有图标的样子。

操作

操作是一个坐落在条上的 tonal 按钮。它的容器是条自身的颜色,上面覆盖一层淡淡的标签颜色,所以在每种形态下,它与条之间都只差同样低调的一级:在表面上或反色,带状态或不带状态。标签是条的文本颜色。

它之所以是 tonal,是因为它通常是“撤销”,刚刚犯了错的用户必须一眼就看出那里有一个按钮。光秃秃的文本标签在条上很容易被忽略。

绝不要把它做成 filled 按钮。 如果一个操作重要到似乎需要 filled 按钮,它就不属于消息条:消息条会消失,而且在焦点顺序中排在最后。请把它放到屏幕的主操作之后,放进底部面板里。

产品可以让这个操作换一种外观。这要在产品的设计系统层里为所有消息条设置一次,方法是给操作的颜色令牌和它的色调层的不透明度赋予别的值;不存在针对单条消息条的变体。Material 和 Cupertino 预设正是这样做的,以保留它们的原版所用的 text 操作。

状态形态

status 图标 朗读方式 用于
default 无 以礼貌(polite)的方式 大多数消息:应用刚刚做了什么。
info info 以礼貌的方式 值得知道、但不是用户操作结果的事。
success check_circle 以礼貌的方式 顺利完成的任务,而且用户正在等它。
warning warning 以立即打断(assertive)的方式 用户不采取行动就可能出问题的事。
error error 以立即打断的方式 用户可以回头再处理的失败,带有一个重试或解释的操作。

状态形态:info、success、warning 和 error

状态形态把条画成该状态的一种淡色,周围有一圈用该状态颜色的细线画出的边线,文本之前是同样颜色的状态图标。这圈边线不占空间。默认形态没有自己的边线,只有它的层级所绘制的那一圈。状态形态从不只靠颜色来区分:图标的形状把各种形态区分开,statusLabel 则用文字给出状态,使用你的产品的语言(“错误”)。这个标签在标题和消息之前朗读。警告和错误会打断正在朗读的内容;其他形态会等它读完。

在表面上,还是反色

默认情况下,消息条绘制在当前配色方案中一个抬升的表面上:也就是对话框所用的那个明亮表面,由它的层级抬起,配以该方案的文本颜色。以反色绘制时,它采用相反配色方案的颜色,在浅色方案中是一个深色的条,这样无论落在什么之上,它都能与之区分开。

以反色绘制:默认形态和四种状态形态

inverse 绘制方式
未设置(null,默认) 由设计系统选择。默认系统选择表面;m3-like 预设选择反色。
true 反色。
false 在表面上,即使系统的选择是反色。

在 Web 上,由这个属性(attribute)表明是三者中的哪一个:省略表示未设置,inverse 表示 true,inverse="false" 表示 false。show() 把 inverse 作为一个选项接收。

以反色绘制的状态形态,就是另一个主题中该状态的形态,每个部件一一对应。在浅色主题中,反色的 info 条就是深色主题所绘制的 info 条,带有那个主题的文本、图标、边线和操作;在深色主题中,它则是浅色主题的那一个。每个对比度级别下都是如此。因此,反色的状态形态不会用到产品在别处没有绘制过的颜色,它的图标和边线也与在表面上时一样鲜明。

以反色绘制只改变颜色,别的都不变:尺寸、圆角、层级、布局、图标、动效、计时、朗读和按键都保持原样。它是一种外观,而不是一种含义。不要用它来表示某条消息很重要;表达这一点的是状态形态和 statusLabel。最好在产品的设计系统层里为整个产品决定一次,让每条消息条看起来都是同一类消息。

长文本

收起时,标题和消息各自最多显示三行,被截断的地方以省略号结尾。此时,操作和关闭按钮之间会出现一个展开按钮。激活它会显示全部文本,触发带有新的 expanded 值的 toggle(在 Web 上是 expandedchange),焦点留在按钮上,消息条不会关闭。条从它的底边向上变高(放在顶部末端时,则从顶边向下变高);在减弱动态效果时,立即变高。它的内容向顶部对齐,所以在条变高或变矮时,图标、第一行以及旁边的控件与顶边的距离保持不变,独占一行的操作与底边的距离也保持不变。你的代码也可以设置 expanded,这不会触发任何事件。

如果展开后的文本仍然比宿主拥有的空间更高,条就占满那个高度,文本在条内滚动,图标和控件留在原位。无论文本是否被截断,辅助技术得到的始终是全部文本。

位置

placement 条所在的位置 适用于
bottom(默认) 窗口底部居中,与底边留一段边距。 大多数产品,以及手机上的所有产品。
top-end 窗口顶部的末端一侧:右上角,在从右到左的布局中是左上角。在紧凑(compact)窗口中(默认系统中宽度小于 600),它改为横跨顶部。 业务应用的桌面屏幕,屏幕底部正是工作所在之处。

为整个产品选定一种位置并交给宿主,让人们知道消息会出现在哪里:<tanrei-snackbar-host> 上的 placement 作用于它显示的每一条消息条,而带着自己的 placement 请求的消息条会保留它。在手机上保持底部,这是 Material 放置消息条的地方,也是拇指所在之处。在业务应用的桌面屏幕上,例如仪表板、管理控制台,或者自己的工具栏、状态栏或表格分页位于底部的编辑器,使用 top-end,这是 Carbon 和 Fluent 放置通知的地方。

在顶部末端,条与顶边和末端边缘各留一段边距,位于安全区域之内、吸顶标题栏之下,所以它从不遮住顶部应用栏。在 Web 上,它从 --tanrei-app-shell-scroll-offset 读取标题栏的高度,这个属性由应用外壳设置在文档的根元素上;有自己框架的产品把这个属性设为自己吸顶标题栏的高度。它从顶边之上下降到位,而不是从底边之下升起;展开的条向下变高。其他一切都不变:仍然一次只显示一条,从不堆叠;它的控件仍然在宿主在焦点顺序中的位置被到达;朗读方式也相同。

<!-- 这个宿主显示的每一条消息条都位于顶部末端。 -->
<tanrei-snackbar-host placement="top-end"></tanrei-snackbar-host>

<script type="module">
  const snackbars = document.querySelector('tanrei-snackbar-host');
  snackbars.show({ message: 'Report exported', action: 'Open' });
  // 单独请求放在底部的消息条保留自己的位置。
  snackbars.show({ message: 'Photo saved', placement: 'bottom' });
</script>
<SnackbarHost ref={snackbars} placement="top-end" />

SwiftUI、Compose、Flutter 和 React Native 还没有顶部末端的位置:它们的消息条位于底部。

时长

duration 它自己停留多久
auto(默认) 没有操作时等同于 short。有操作时它永不超时,所以没有人会因为超时而错过操作(WCAG 2.2.1)。
short 在短超时之后关闭(默认系统中是 4 秒)。
long 在长超时之后关闭(默认系统中是 10 秒)。
indefinite 一直停留,直到用户或你的代码把它关闭。

计时的消息条在以下情况下会暂停:指针位于它上方时、焦点在它里面时、它处于展开状态时、应用被隐藏或处于后台时,以及被 Tanrei 的模态对话框盖住时。之后它从完整的时长重新开始计时。如果 Android 的无障碍超时更长,时长就延长到那个值;屏幕阅读器运行期间,带操作的消息条永不超时。如果你让带操作的消息条超时,请在别处也提供这个操作,例如菜单里的“撤销”。调试构建会提醒你。

显示、焦点与关闭

消息条是供阅读的浮层。用户只是读取它的消息,所以显示它从不移动焦点。

  • 一次一条。 宿主按请求的顺序显示消息条。下一条要等上一条淡出之后才显示。在 Tanrei 的模态对话框打开期间请求的消息条,要等对话框关闭之后才显示。
  • 位置。 底部居中,与底边和两侧各留一段边距,位于安全区域之内、屏幕键盘之上;或者在 placement="top-end" 时,位于顶部末端、吸顶标题栏之下(见位置)。无论哪种,它都绘制在所在区域的内容之上,位于模态对话框及其遮罩之下。
  • 朗读。 它出现时,辅助技术会把状态标签、标题和消息朗读一次,接着是操作的标签:以礼貌(polite)的方式朗读,也就是等正在朗读的内容读完;对于警告和错误,则以立即打断(assertive)的方式朗读。没有任何东西夺取键盘焦点或屏幕阅读器焦点。
  • 焦点。 操作、展开按钮和关闭按钮通过 Tab 到达,位置就是宿主在焦点顺序中的位置,这正是宿主要放在最后的原因。条本身不是 Tab 停靠点;只有当展开的条需要滚动文本时,它的文本才是。
  • 关闭。 消息条在它的操作被激活时关闭(action),或在没有激活操作的情况下因以下原因之一关闭(dismiss):
原因 何时
close 关闭按钮被激活。
escape 焦点在消息条内时按下了 Escape。
timeout short 或 long 的消息条时间用完了。
host 你的代码关闭了它,或清空了队列。
  • 焦点交还。 如果消息条关闭时焦点在它里面,焦点就交还给焦点进入消息条之前所在的那个元素。
  • 动效。 它从底边之下上升到位(在顶部末端则从顶边之上下降到位),同时淡入;淡出时留在原位。在减弱动态效果时,它只淡入淡出,不移动;Android 的这项设置会移除所有动画,所以在那里它立即出现、立即消失。

推荐

  • 用已经完成的口吻说明发生了什么:“消息已发送”“已移动 3 个文件”。
  • 给操作一个简短的动词,也就是用户看到消息后会期待的那个:“撤销”“查看”“重试”。
  • 选择与所发生的事相符的状态,并为每个状态形态提供 statusLabel。
  • 为整个产品决定一次:消息条是绘制在表面上,还是以反色绘制,以及它放在哪里。
  • 让文本足够短,使它很少需要展开按钮。
  • 没有理由就保留默认的 auto 时长。它从不让任何人因超时而错过操作。
  • 把宿主放在页面或布局的最后,让它的控件在焦点顺序中排在最后。

避免

  • 不要用消息条来显示阻碍任务的错误,也不要用它提问。请使用对话框。
  • 不要在消息条里放两个操作。它只容得下一个。
  • 不要把操作做成 filled 按钮,也不要把看起来需要 filled 按钮的操作放进消息条。请使用从主操作打开的底部面板。
  • 不要把警告或错误形态用于日常的确认。它们会打断屏幕阅读器。
  • 不要为了标明重要性而以反色绘制消息条。反色只改变颜色;说明消息属于哪一类的是状态。
  • 不要为屏幕上已经清楚显示的事再显示消息条。
  • 不要把焦点移到消息条或它的操作上。它不能把用户从工作中带走。

无障碍

角色
status
标注
The snackbar is a live region: polite in the default, info and success forms (the status role on the web) and assertive in the warning and error forms (the alert role on the web). When it appears it announces, once, statusLabel, the title, the whole message and the action's label. The status glyph is hidden from assistive technology; statusLabel gives the status in words. The action's name is its label; the close button's name is closeLabel; the expand button's name is expandLabel, and it exposes whether the text is expanded. The bar itself is not a tab stop; its text is one only while it scrolls.
最小目标
即使外观尺寸更小,点击区域也满足最小目标尺寸。 minimum 32px · pointer 44px · spacing 8px · touch 48px

键盘操作

按键 动作
Tab Reaches, at the host's place in the focus order, the text while it scrolls, then the action, the expand button and the close button. Showing a snackbar never moves focus.
Enter / Space Activates the focused action, expand button or close button.
Arrow keys, Page Up, Page Down While the expanded text scrolls and has focus, scroll it.
Escape While focus is inside the snackbar, closes it (dismiss, reason escape).

备注

  • WCAG 2.2.1 (timing adjustable): by default (auto) a snackbar with an action never times out. A product may time one out (short or long), but then the action must also be available elsewhere (Undo in a menu or a history), because a person who is slow, zoomed-in or using a screen reader may not reach it in time. The timeout pauses while the snackbar is hovered, focused or expanded, and Android's accessibility timeout lengthens it.
  • A snackbar reports; it does not ask. A choice between two actions, an error that blocks the person's work, or anything they must acknowledge belongs in a dialog or next to what it is about. The error form is for a failure the person can come back to (a message not sent, with Retry).
  • The action is a tonal button so that an undo is seen at once: its label is the strongest thing in it (at least 9.1:1 on the action at rest and 6.9:1 in its weakest state, in every form and scheme of the default system), and the action is one quiet step off its bar, the same step in every form (1.25–1.45:1 against the bar in all ten forms and six schemes), so it never outweighs the message and never weighs more on one status than on another. A button with a visible label needs no boundary contrast (WCAG 1.4.11). An action important enough to seem to want a filled button does not belong in a snackbar: put a Main Action Button in a bottom sheet instead.
  • WCAG 2.4.13 and 1.4.11 (focus appearance, non-text contrast): the focus indicator is drawn on the bar, so its color is the form's (SNK-3). On the default form's surface it is the system's focus color, which the build holds against every surface role; on a status bar and on an inverse bar it is that bar's content color, which the build holds against the bar at 4.5:1 or more. The system's focus color alone is held against no status bar and falls under 3:1 on the inverse surface in five of the six schemes.
  • Drawn inverse or not, a snackbar is the same message: the role, the announcement, the focus order and the timing do not change with its colors (SNK-30). A product should not use inverse to mean something (an inverse bar for important messages): that is what the status forms and statusLabel are for.
  • WCAG 1.4.1 (use of color): a status form is never told by color alone. It draws the status's glyph, a shape of the icon set, and gives statusLabel, the status in words, to assistive technology (SNK-21).
  • WCAG 1.4.3 and 1.4.11 (contrast): three parts are drawn at an opacity of their own color, so their contrast is that of the color as drawn, not of the role. In the default system the message under a title keeps at least 7.1:1 on its bar (7.6:1 in the high-contrast schemes) and the status glyph at least 3.6:1, in every form and scheme (SNK-32, SNK-21); a status form's edge is decoration beside the glyph and statusLabel and is held to nothing (SNK-31). A system that changes one of these opacities, or the colors under them, must keep 4.5:1 for the message and 3:1 for the glyph.
  • WCAG 1.4.11 (non-text contrast): the expand and close buttons are Buttons in the form's tone (SNK-34): the glyph is the bar's message color, and the circle under it is that color at comp.button.tone.container.opacity over the bar. In the default system the glyph keeps at least 8.0:1 against its circle at rest and 6.2:1 in its weakest states (focused and pressed), in all ten forms and six schemes — well above the 3:1 a glyph without a label needs. A system that changes the tone's opacity, the state-layer opacities or a form's colors must keep 3:1 for the glyph against the circle in every state.
  • Text size never hides text for good: a collapsed title or message ends with an ellipsis at its line limit and the expand button shows the rest (SNK-23); the action moves to its own row (SNK-16); an expanded bar taller than the window scrolls inside it (SNK-25).
  • Web: `<tanrei-snackbar-host>` is placed by the product, normally as the last element of the page (its place in the focus order). It shows `<tanrei-snackbar>` elements as `popover="manual"`, so they are in the top layer, and exposes `show(options): Promise<{ result: 'action' | 'dismiss', reason? }>` (whose options include `inverse`), `dismissCurrent()` and `clear()`. The live regions are the host's two, which are always in the document, so screen readers register them before the first message is inserted: one with the status role (polite) for the default, info and success forms, one with the alert role (assertive) for warning and error. The expand button carries `aria-expanded` and `aria-controls` naming the text.
  • SwiftUI, Compose, Flutter and React Native announce with the platform's own call, at default priority for the polite forms and at high priority for warning and error: AccessibilityNotification.Announcement with the speech announcement priority, a polite or assertive liveRegion, SemanticsService.announce with its assertiveness, and AccessibilityInfo.announceForAccessibility (announceForAccessibilityWithOptions with queue off for assertive on iOS). None of them takes focus. The expand button's state is the platform's expanded/collapsed semantics (Compose's expand and collapse actions, Flutter's expanded flag, React Native's accessibilityState.expanded, and on Apple platforms the expanded and collapsed value VoiceOver reads for a disclosure).
  • How a state looks is a value decision of the active design system; components read the state's tokens and never compute the effect themselves.

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

组件负责的部分

  • 消息条通过实时区域(live region)朗读:默认、info 和 success 形态使用礼貌的实时区域(在 Web 上是 status 角色),warning 和 error 使用立即打断的实时区域(alert 角色)。它出现时,状态标签、标题和消息,然后是操作的标签,会被朗读一次,而且总是完整的,即使文本被截断也是如此。在 SwiftUI、Compose、Flutter 和 React Native 上使用平台自己的朗读机制,对 warning 和 error 采用高优先级;它们都不夺取焦点。
  • 状态形态从不只靠颜色来区分:它绘制状态的图标,并把 statusLabel 作为标题之前的文本提供给辅助技术。图标本身对辅助技术隐藏,展开和关闭的图标也是如此。
  • 操作以它的标签命名,展开按钮以 expandLabel 命名,关闭按钮以 closeLabel 命名。它们都通过 Tab 到达,位置是宿主在焦点顺序中的位置,并用 Enter 或 Space 激活。
  • 展开按钮会表明文本是否处于展开状态,并在两种状态下保持相同的名称。如果展开的条需要滚动文本,文本就成为一个焦点停靠点,以标题命名,用方向键以及 Page Up 和 Page Down 滚动。
  • 焦点在消息条内时,Escape 会关闭它。如果它关闭时焦点在里面,焦点就交还到原先所在的位置。
  • 条所在的位置不改变其他任何东西。位于顶部末端的消息条同样在宿主在焦点顺序中的位置通过 Tab 到达,朗读方式与位于底部的完全相同,而且从不遮住吸顶标题栏。
  • 带操作的消息条默认不会超时。计时的消息条在悬停、聚焦或展开时暂停,会延长到 Android 的无障碍超时;如果带有操作,在屏幕阅读器运行期间永不超时。
  • 操作在两个方向上都满足最小目标,而绘制出来的尺寸不变;展开按钮和关闭按钮是 Button,保持 Button 自己的最小目标。每个焦点环都只在键盘聚焦时显示,颜色取自消息条所处的形态,所以在每一种条上都看得见:表面、状态的淡色和反色的条。
  • 展开按钮和关闭按钮的图标,在每一种状态和形态下,对下面的圆都保持至少 3:1。在默认系统中,静止时至少 8.0:1,聚焦和按下时至少 6.2:1。
  • 画得较淡的部件,在默认系统的每个配色方案中都保持相对于条的对比度:状态图标至少 3:1,标题之下的消息至少 4.5:1,在高对比度方案中则是 7:1。变淡只是颜色上的事;辅助技术朗读的内容不变,消息条以反色绘制时也不变。
  • 文本变大时,不会有文本被永久隐藏:收起的标题或消息在三行处以省略号结尾,其余部分由展开按钮显示;操作移到单独的一行;展开后比窗口还高的条可以滚动。
  • 在图标字体加载完成之前,任何图标的名称都不会被画成文本:图标的位置保持空白,直到字体能够把它画出来。
  • 调试构建会报告空的消息、没有 closeLabel 的关闭按钮、没有 statusLabel 的状态形态、没有 expandLabel 的展开按钮,以及带有操作的计时消息条。

已知的平台差距:

  • Web:浏览器不会告知是否有屏幕阅读器在运行,也没有无障碍超时,所以两者都不会延长计时的消息条。让操作始终够得着的,是默认的 auto 时长。
  • Web:避让屏幕键盘这一点,尚未在带有屏幕键盘的设备上检查过。
  • SwiftUI 无法把焦点交还给原先持有它的元素:消息条关闭时如果焦点在里面,焦点会落在 SwiftUI 为被移除的视图安排的位置。如果宿主所在的视图由你托管在场景之外(例如你自己的 NSHostingView),它的超时永远不会运行。
  • Compose 无法指出焦点进入消息条之前所在的元素。消息条在持有焦点的情况下关闭时,焦点沿着进来的路退出去:如果是按 Tab 进来的,就回到宿主之前的那个元素。如果是你的代码在焦点位于里面时把它关闭,焦点不会移动。
  • Compose:延长到 Android 的无障碍超时,以及窗口隐藏时暂停,尚未被测试覆盖。
  • Compose:礼貌的朗读、窗口没有焦点时的暂停以及无障碍超时,都只在 Robolectric 上测试过,尚未在设备上测试。
  • Flutter 还不会把计时的消息条延长到 Android 的无障碍超时,因为 Flutter 没有暴露这个值。
  • React Native 无法得知先前是哪个元素持有焦点。消息条关闭时如果焦点在它里面,焦点会落在平台安排的位置。
  • React Native 没有自己的安全区域 API。只有当你把窗口的边衬区传给 TanreiProvider 时,消息条才会保持在安全区域之内;没有传入时,在带有主屏幕指示条的手机上,它可能有一部分盖在指示条上。
  • React Native 根据排好的文本行来决定操作是否移到单独的一行,所以这次移动发生在第一轮布局之后。
  • React Native:礼貌的朗读、保持在键盘上方,以及因进入后台和指针悬停而暂停,都尚未在设备上检查过。

需要你做的事

  • 把消息写成纯文本,让它在被单独朗读出来时也说得通。
  • 为关闭按钮提供 closeLabel,为状态形态提供 statusLabel,为文本可能较长的消息条提供 expandLabel,全都使用你的产品的语言。
  • 把宿主放在页面的最后;在 Compose 上,放在布局的最后。
  • 在 React Native 上,把窗口的 insets 传给 TanreiProvider,让消息条避开系统栏。
  • 如果你为带操作的消息条选择了 short 或 long,请在某个不会超时的地方提供同样的操作。
  • 凡是用户必须处理的事,都放进对话框,而不是消息条。