跳到主要内容
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.main-action-button
层级
基础

概览

Web
tanrei-main-action-button
schedule
SwiftUI
TanreiMainActionButton
radio_button_unchecked
Jetpack Compose
TanreiMainActionButton
radio_button_unchecked
Flutter
TanreiMainActionButton
radio_button_unchecked
React Native
MainActionButton
radio_button_unchecked

生成自 docs/status/status.json, specs/main-action-button.spec.json

展开与收起

editedit

主操作按钮代表屏幕为之存在的那一个操作:邮箱里的“撰写”,日历里的“新建日程”,列表里的“添加”。它画在一个悬浮于内容之上的抬升容器上,所以内容在它下方滚动时,它始终触手可及。

它是一个按钮,有两种形态。展开(extended,默认)显示图标和标签。收起(collapsed)只显示图标,放在一个与展开形态等高的正方形里,在默认系统中是圆形。由产品让它在两种形态之间变化,例如内容滚动时收起,回到顶部时再展开。它是一个操作,绝不是切换按钮:收起改变的是它的形状,而不是它的含义,标签在两种形态下都是它的无障碍名称。

在 Web 上它是 <tanrei-main-action-button>,在 React 中是 MainActionButton。SwiftUI、Compose、Flutter 和 React Native 尚未实现。

用法

标签是一个属性,而不是内容,因为两种形态都需要它:展开时画出来,并且始终是无障碍名称。图标是图标集中的一个图标。按钮不会自己定位:由你的布局来放置它,通常放在屏幕末尾侧的底角,并避开导航栏。

<tanrei-main-action-button class="primary-action" label="撰写">
  <span slot="icon">edit</span>
</tanrei-main-action-button>

<!-- 收起:这个属性(attribute)与 extended 属性(property)含义相反。 -->
<tanrei-main-action-button label="撰写" collapsed><span slot="icon">edit</span></tanrei-main-action-button>

<!-- 指向操作去向的图标放在标签之后。 -->
<tanrei-main-action-button label="下一步" icon-position="trailing"><span slot="icon">arrow_forward</span></tanrei-main-action-button>

<style>
  /* 元素自己不做任何定位:由页面把它放在末尾侧的底角。 */
  tanrei-main-action-button.primary-action {
    position: fixed;
    inset-inline-end: var(--tanrei-sys-space-space400);
    inset-block-end: var(--tanrei-sys-space-space400);
  }
</style>

<script type="module">
  import '@tanrei/web/components/main-action-button';
  const compose = document.querySelector('tanrei-main-action-button.primary-action');
  compose.addEventListener('click', newMessage);
  // extended 是一个默认为 true 的属性;从外部把它收起,例如在页面滚动之后。
  addEventListener('scroll', () => (compose.extended = scrollY === 0), { passive: true });
</script>
import { MainActionButton } from '@tanrei/react';

<MainActionButton className="primary-action" label="撰写" extended={atTop} onClick={newMessage}>
  <span slot="icon">edit</span>
</MainActionButton>

<MainActionButton variant="tonal" label="下一步" iconPosition="trailing" onClick={next}>
  <span slot="icon">arrow_forward</span>
</MainActionButton>

FAB 和扩展 FAB 的去向

主操作按钮取代了悬浮操作按钮(floating action button)和扩展 FAB。过去由产品来回替换的两个组件,现在是同一个组件的两种形态。

之前 现在
悬浮操作按钮(fab) 收起的主操作按钮:extended 设为 false
扩展 FAB(extended-fab) 展开的主操作按钮(默认)
扩展 FAB 的 expanded extended
comp.fab.*、comp.extended-fab.* comp.main-action-button.*

Material 3 的名称

  • Material 3 的 FAB、small FAB 和 large FAB 分别是 medium、small 和 large 尺寸的收起的主操作按钮。它的 extended FAB 是展开形态。
  • Compose 的 expanded(在 ExtendedFloatingActionButton 上)和 Flutter 的 isExtended(在 FloatingActionButton.extended 上)就是 extended。
  • Material 3 的 primary、secondary、tertiary 和 surface 这几种 FAB 颜色,对应 filled、tonal 和 surface 变体,以及预设添加的值。m3-like 预设通过这个组件的令牌,画出 Material 的圆角方形和 56 / 96 的高度。
  • 用 Material 的说法,主操作按钮就是 FAB。Material 的 FAB 菜单就是 FAB 菜单,它是 m3-like 层的组件:继承这一层的系统都有它。

规格

变体

editeditedit

尺寸,展开与收起

editediteditediteditedit

后置图标、没有图标,以及禁用

arrow_forwardeditedit

构成

构成

绘制在 Web 包所绘的测试样例 filled-extended 之上。
部件 说明
1 container The raised shape that holds the icon and the label, casts the shadow and carries hover, focus and press. Extended it is as wide as its content; collapsed it is a square of its height, a circle under full corners.
2 icon · 可选 The glyph that names the action, at the leading or the trailing side of the label; drawn in the label color. Required to collapse.
3 label · 可选 Text that names the action. Shown while extended; hidden, but still the accessible name, while collapsed. Required to extend.
4 focus-ring Gap-then-ring indicator shown for keyboard focus.

尺寸

尺寸

长度 令牌 值(px)
container padding-inline-start-with-icon tanrei.comp.main-action-button.container.padding-inline-start-with-icon.medium 16
container gap tanrei.comp.main-action-button.container.gap.medium 12
container padding-inline-end tanrei.comp.main-action-button.container.padding-inline-end.medium 20
container height tanrei.comp.main-action-button.container.height.medium 56
icon size tanrei.comp.main-action-button.icon.size.medium 24
绘制在 Web 包所绘的测试样例 filled-extended 之上。

属性

属性 类型 默认值 说明
variant filled | tonal | surface "filled" Emphasis. Filled is the screen's main action at full strength; tonal is quieter; surface keeps the container neutral where a colored one would compete with the content beneath (a map, a photo). A design system may add values.
size small | medium | large "medium" Visual height (40 / 56 / 96), which is also the side of the collapsed form, so one button keeps its height while it collapses and extends. Small sits beside a medium one for a related action; large marks the action on a wide or empty screen. The hit area always meets the minimum target. A design system may add values.
extended boolean true Shows the label beside the icon. False collapses the button to its icon (for example while the content scrolls); the label stays the accessible name. It changes the button's form, never its meaning: this is not a toggle. A button without an icon stays extended, and one without a label never extends (MAB-15, MAB-16).
fullWidth boolean false Makes the button take the whole width its container gives it, its icon and label centred: the one action of a screen held across its foot on a narrow screen (MAB-24). A full-width button is always drawn extended, so extended is ignored while it is true.
iconPosition leading | trailing "leading" The side of the label the icon is on, in the reading direction. Trailing is for a glyph that points where the action goes, such as the arrow of Next. Collapsed, the icon is centered whichever side it is on.
disabled boolean false Prevents interaction and removes the button from the focus order. Hiding it when its action is unavailable is usually better than disabling it.
label string "" The visible text while extended, and the accessible name in either form. Required: an icon alone is not a name.

插槽

插槽 说明
icon The icon (a glyph of the system's icon set).

事件

事件 说明
click The button was activated by pointer, touch, keyboard or assistive technology.

状态

行为

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

ID 需求
MAB-1 WHEN the user activates the main action button by pointer, touch, Enter or Space THE main action button SHALL emit click exactly once.
MAB-2 WHILE disabled is true THE main action button SHALL NOT emit click, SHALL NOT be focusable and SHALL expose the disabled state to assistive technology.
MAB-3 WHILE hovered, keyboard-focused or pressed THE main action button SHALL render its container, label, icon, elevation and opacity from the component tokens of the highest-precedence active state, with precedence pressed over focused over hovered; focus gained by pointer or touch SHALL NOT select the focused state.
MAB-4 WHEN the main action button receives keyboard focus THE main action button SHALL show the focus ring without animation; focus gained by pointer or touch SHALL NOT show it.
MAB-5 WHERE the visual size is below the minimum target THE main action button SHALL extend its 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.
MAB-6 WHILE disabled is true THE main action button SHALL render its container, label, icon, elevation and opacity from the disabled component tokens of its variant.
MAB-7 WHEN variant or size names a value that the active design system does not have THE main action button SHALL render with the default value's tokens, and in debug builds report the unknown value.
MAB-8 WHEN the main action button is pressed by pointer, touch, Enter or Space THE main action button 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.
MAB-9 WHILE pressed THE main action button SHALL answer as sys.behavior.pressed-feedback names: overlay applies the pressed tokens to the whole container 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 control 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; label, icon, elevation, 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.main-action-button.motion.feedback-duration with comp.main-action-button.motion.state-easing, fades over the state duration of its size (comp.main-action-button.motion.state-duration.small, .medium or .large) with comp.main-action-button.motion.state-easing once the press has ended and press-hold has passed, and appears whole where motion is reduced.
MAB-10 THE main action button SHALL ease its container 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.
MAB-11 WHILE it is extended THE main action button SHALL draw a container at least the height of its size (comp.main-action-button.container.height.medium for medium, and so on) with the corners of its size (comp.main-action-button.container.shape.medium, and so on), holding its label and, when it has one, its icon on the side iconPosition names (leading: before the label in the reading direction; trailing: after it), the two separated by comp.main-action-button.container.gap.medium (and so on); it SHALL pad the icon's side by comp.main-action-button.container.padding-inline-start-with-icon.medium for a leading icon or comp.main-action-button.container.padding-inline-end-with-icon.medium for a trailing one, a start without an icon by comp.main-action-button.container.padding-inline-start.medium and an end without an icon by comp.main-action-button.container.padding-inline-end.medium, and the top and bottom by at least comp.main-action-button.container.padding-block.medium (and so on for each size), and SHALL be as wide as that content.
MAB-12 THE main action button SHALL set its label in the text style of its size (comp.main-action-button.label.typescale.medium, and so on) and the label color of its variant and state (comp.main-action-button.filled.label.color for filled at rest, and so on), SHALL NOT truncate it (it stays on one line while it fits, wraps when the layout gives it less width, and the container grows in both axes with it), and SHALL draw the icon, in either form, in the same color at the icon size of its size (comp.main-action-button.icon.size.medium, and so on) 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.
MAB-13 WHILE it is collapsed THE main action button SHALL hide its label and draw a square container whose side is the height of its size (comp.main-action-button.container.height.medium, and so on) with the corners of its size (comp.main-action-button.container.shape.medium, and so on; a circle where they are full, as in the default system) and its icon centered in it whichever side iconPosition names, and SHALL keep label as its accessible name.
MAB-14 WHEN extended changes THE main action button SHALL change its width between the two forms over comp.main-action-button.motion.resize-duration with comp.main-action-button.motion.resize-easing, keeping its height and corners, fading the label out as it narrows and in as it widens, and moving the icon between its side of the label and the center, and SHALL change at once where motion is reduced; which edge stays in place is decided by the layout that holds it, as for any change of its width.
MAB-15 IF the main action button has no icon THEN THE main action button SHALL draw its extended form whatever extended says (there is no empty collapsed form), and in debug builds report it while extended is false.
MAB-16 WHILE label is not empty THE main action button SHALL expose it as its accessible name, in either form, over a name the product gives it on the element (aria-label or aria-labelledby). IF label is empty THEN THE main action button SHALL draw its collapsed form whatever extended says, SHALL expose the name the product gives it on the element — aria-label, or aria-labelledby naming an element — as its accessible name, and WHILE the product gives it none SHALL in debug builds report that it has no accessible name; this holds over MAB-15 when it has no icon either.
MAB-17 THE main action button SHALL cast the elevation of its variant and state (comp.main-action-button.filled.container.elevation for filled at rest, and so on) outside its container without clipping it, in either form and while it changes between them, and WHEN the elevation or the colors change between states SHALL animate the change over the state duration of its size (comp.main-action-button.motion.state-duration.small, .medium or .large) with comp.main-action-button.motion.state-easing, changing them at once where motion is reduced.
MAB-18 THE main action button SHALL NOT position itself: it SHALL lay out at its own size, or across the width of its container while fullWidth is true (MAB-24), where the layout that holds it places it (normally the inline-end bottom corner of the screen, clear of any navigation bar), and SHALL NOT draw a scrim or move content.
MAB-19 WHEN the layout direction is right-to-left THE main action button SHALL mirror the order of its icon and label (a leading icon on the right, a trailing one on the left) and apply its paddings by logical start and end, SHALL NOT mirror the glyph itself (the icon helper mirrors a glyph the icon set marks, MAB-22), and SHALL grow a ripple from the point of contact as in left-to-right.
MAB-20 THE main action button SHALL expose itself to assistive technology as a button named by label with no pressed, selected, checked or expanded state, and WHEN extended changes SHALL NOT change its role, its name or its focus, and SHALL NOT announce the change.
MAB-21 THE main action button 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 main action button is shown THE main action button SHALL draw itself for the new direction at once, without a motion of its own, keeping what it shows and its state.
MAB-22 WHERE the layout direction is right-to-left THE main action button SHALL draw the glyph of its icon slot 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 MAB-19 gives. THE main action button SHALL NOT mirror a glyph itself, and SHALL NOT mirror content the product gives as an image or a view of its own rather than by a glyph's name.
MAB-23 THE main action button SHALL lay out its 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 main action button SHALL still place the label and align its lines by the layout direction, not by the text's own.
MAB-24 WHILE fullWidth is true THE main action button SHALL take the whole inline size its container gives it and SHALL draw its extended form whatever extended says — the height, corners, paddings, elevation and states of MAB-11 and MAB-17 — with its icon and label centred in that width as one row, in the order MAB-19 gives them; WHERE that width is narrower than the label THE main action button SHALL wrap the label, its lines centred (MAB-12). WHEN extended changes while fullWidth is true THE main action button SHALL NOT change its form, and in debug builds SHALL report that a full-width button does not collapse. MAB-16 holds over this rule: a button with no label draws its collapsed form, at its own size, and is reported.
MAB-25 WHERE the platform has ARIA attributes (the web), WHEN the product sets an aria- attribute on the host, by the attribute or by the ARIA property that reflects it, THE main action button SHALL move it to the data-aria- attribute of the same name, so that the host is no second node with its name, description or state, and SHALL expose it on the element that carries its role — an id reference by the elements it names in the host's tree or a tree around it, an error message as part of the description while the main action button is invalid — except those the main action button owns (aria-disabled), which it SHALL move and not expose. WHEN the product removes the aria- attribute or its data-aria- twin, or sets the ARIA property that reflects it (ariaLabel, ariaDescribedByElements…) to null, THE main action button SHALL take it off that element. WHEN the product sets an aria- attribute the main action button owns THE main action button SHALL report it in debug builds.

各平台名称对照

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

规格 Web SwiftUI Jetpack Compose Flutter React Native
组件 tanrei-main-action-button TanreiMainActionButton TanreiMainActionButton TanreiMainActionButton MainActionButton
variant · 属性 variant variant variant variant variant
size · 属性 size size size size size
extended · 属性 extended isExtended expanded isExtended extended
fullWidth · 属性 fullWidth fullWidth fullWidth fullWidth fullWidth
iconPosition · 属性 iconPosition iconPosition iconPosition iconPosition iconPosition
disabled · 属性 disabled disabled enabled onPressed disabled
label · 属性 label title text label label
click · 事件 click action onClick onPressed onPress
备注 The icon is the icon slot (a glyph of the icon font). extended is a boolean property that defaults to true; its attribute is the inverse collapsed, because an HTML boolean attribute cannot default to true. iconPosition is the icon-position attribute. The host has no aria-expanded or aria-pressed (MAB-20). The element is inline; the page positions it (for example with position: fixed or a sticky container). fullWidth is the full-width attribute: the host is then a block-level flex box as wide as its container, and the native <button> fills it (MAB-24). The initializer takes the title first, as Button(_:action:) does, and an optional icon name. disabled follows .disabled(_:). The view is placed by the caller, typically in an .overlay(alignment: .bottomTrailing). Compose idiom (as Material 3's extended FAB composable): expanded collapses it; enabled is the inverse flag; text is a String so it is also the content description. Placed through Scaffold's floatingActionButton slot or by the caller. Flutter idiom (as Material 3's extended FAB): isExtended collapses it and a null onPressed disables it. Placed through Scaffold.floatingActionButton or by the caller; it adds no Hero. The icon is the icon prop (a glyph name). The caller positions it (for example absolutely inside the screen's root view).

令牌

令牌 引用 解析值(浅色)
--tanrei-comp-main-action-button-container-gap-large sys.space.space200 16px
--tanrei-comp-main-action-button-container-gap-medium sys.space.space150 12px
--tanrei-comp-main-action-button-container-gap-small sys.space.space100 8px
--tanrei-comp-main-action-button-container-height-large — 96px
--tanrei-comp-main-action-button-container-height-medium — 56px
--tanrei-comp-main-action-button-container-height-small — 40px
--tanrei-comp-main-action-button-container-padding-block-large — 6px
--tanrei-comp-main-action-button-container-padding-block-medium — 6px
--tanrei-comp-main-action-button-container-padding-block-small — 6px
--tanrei-comp-main-action-button-container-padding-inline-end-with-icon-large sys.space.space400 32px
--tanrei-comp-main-action-button-container-padding-inline-end-with-icon-medium sys.space.space250 20px
--tanrei-comp-main-action-button-container-padding-inline-end-with-icon-small sys.space.space150 12px
--tanrei-comp-main-action-button-container-padding-inline-end-large sys.space.space400 32px
--tanrei-comp-main-action-button-container-padding-inline-end-medium sys.space.space250 20px
--tanrei-comp-main-action-button-container-padding-inline-end-small sys.space.space200 16px
--tanrei-comp-main-action-button-container-padding-inline-start-with-icon-large sys.space.space300 24px
--tanrei-comp-main-action-button-container-padding-inline-start-with-icon-medium sys.space.space200 16px
--tanrei-comp-main-action-button-container-padding-inline-start-with-icon-small sys.space.space150 12px
--tanrei-comp-main-action-button-container-padding-inline-start-large sys.space.space400 32px
--tanrei-comp-main-action-button-container-padding-inline-start-medium sys.space.space250 20px
--tanrei-comp-main-action-button-container-padding-inline-start-small sys.space.space200 16px
--tanrei-comp-main-action-button-container-shape-large sys.shape.corner.full 9999px
--tanrei-comp-main-action-button-container-shape-medium sys.shape.corner.full 9999px
--tanrei-comp-main-action-button-container-shape-small sys.shape.corner.full 9999px
--tanrei-comp-main-action-button-disabled-opacity — 1
--tanrei-comp-main-action-button-filled-container-color sys.color.primary #1b1b1b
--tanrei-comp-main-action-button-filled-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-main-action-button-filled-disabled-container-color — #1b1b1b · 12%
--tanrei-comp-main-action-button-filled-disabled-container-elevation —
--tanrei-comp-main-action-button-filled-disabled-label-color — #1b1b1b · 38%
--tanrei-comp-main-action-button-filled-disabled-state-layer-color — #1b1b1b · 38%
--tanrei-comp-main-action-button-filled-focused-container-color — #323232
--tanrei-comp-main-action-button-filled-focused-container-elevation — 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-main-action-button-filled-focused-label-color — #ffffff
--tanrei-comp-main-action-button-filled-focused-state-layer-color — #ffffff
--tanrei-comp-main-action-button-filled-hovered-container-color — #2d2d2d
--tanrei-comp-main-action-button-filled-hovered-container-elevation — 0px 0px 0px 1px rgb(0 0 0 / 0.08), 0px 1px 1px 0px rgb(0 0 0 / 0.04), 0px 8px 16px -4px rgb(0 0 0 / 0.04), 0px 24px 32px -8px rgb(0 0 0 / 0.06)
--tanrei-comp-main-action-button-filled-hovered-label-color — #ffffff
--tanrei-comp-main-action-button-filled-hovered-state-layer-color — #ffffff
--tanrei-comp-main-action-button-filled-label-color sys.color.on-primary #ffffff
--tanrei-comp-main-action-button-filled-pressed-container-color — #323232
--tanrei-comp-main-action-button-filled-pressed-container-elevation — 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-main-action-button-filled-pressed-label-color — #ffffff
--tanrei-comp-main-action-button-filled-pressed-state-layer-color — #ffffff
--tanrei-comp-main-action-button-filled-state-layer-color comp.main-action-button.filled.label.color #ffffff
--tanrei-comp-main-action-button-focused-opacity — 1
--tanrei-comp-main-action-button-hovered-opacity — 1
--tanrei-comp-main-action-button-icon-size-large sys.icon.large 32px
--tanrei-comp-main-action-button-icon-size-medium sys.icon.medium 24px
--tanrei-comp-main-action-button-icon-size-small sys.icon.medium 24px
--tanrei-comp-main-action-button-motion-feedback-duration sys.motion.role.feedback 450ms
--tanrei-comp-main-action-button-motion-resize-duration sys.motion.role.resize.large 400ms
--tanrei-comp-main-action-button-motion-resize-easing sys.motion.easing.spatial cubic-bezier(0.2, 0, 0, 1)
--tanrei-comp-main-action-button-motion-state-duration-large sys.motion.role.state.large 250ms
--tanrei-comp-main-action-button-motion-state-duration-medium sys.motion.role.state.default 250ms
--tanrei-comp-main-action-button-motion-state-duration-small sys.motion.role.state.compact 200ms
--tanrei-comp-main-action-button-motion-state-easing sys.motion.easing.state cubic-bezier(0.2, 0, 0, 1)
--tanrei-comp-main-action-button-pressed-opacity — 1
--tanrei-comp-main-action-button-surface-container-color sys.color.surface-container-lowest #ffffff
--tanrei-comp-main-action-button-surface-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-main-action-button-surface-disabled-container-color — #1b1b1b · 12%
--tanrei-comp-main-action-button-surface-disabled-container-elevation —
--tanrei-comp-main-action-button-surface-disabled-label-color — #1b1b1b · 38%
--tanrei-comp-main-action-button-surface-disabled-state-layer-color — #1b1b1b · 38%
--tanrei-comp-main-action-button-surface-focused-container-color — #e8e8e8
--tanrei-comp-main-action-button-surface-focused-container-elevation — 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-main-action-button-surface-focused-label-color — #1b1b1b
--tanrei-comp-main-action-button-surface-focused-state-layer-color — #1b1b1b
--tanrei-comp-main-action-button-surface-hovered-container-color — #ededed
--tanrei-comp-main-action-button-surface-hovered-container-elevation — 0px 0px 0px 1px rgb(0 0 0 / 0.08), 0px 1px 1px 0px rgb(0 0 0 / 0.04), 0px 8px 16px -4px rgb(0 0 0 / 0.04), 0px 24px 32px -8px rgb(0 0 0 / 0.06)
--tanrei-comp-main-action-button-surface-hovered-label-color — #1b1b1b
--tanrei-comp-main-action-button-surface-hovered-state-layer-color — #1b1b1b
--tanrei-comp-main-action-button-surface-label-color sys.color.primary #1b1b1b
--tanrei-comp-main-action-button-surface-pressed-container-color — #e8e8e8
--tanrei-comp-main-action-button-surface-pressed-container-elevation — 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-main-action-button-surface-pressed-label-color — #1b1b1b
--tanrei-comp-main-action-button-surface-pressed-state-layer-color — #1b1b1b
--tanrei-comp-main-action-button-surface-state-layer-color comp.main-action-button.surface.label.color #1b1b1b
--tanrei-comp-main-action-button-tonal-container-color sys.color.secondary-container #eeeeee
--tanrei-comp-main-action-button-tonal-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-main-action-button-tonal-disabled-container-color — #1b1b1b · 12%
--tanrei-comp-main-action-button-tonal-disabled-container-elevation —
--tanrei-comp-main-action-button-tonal-disabled-label-color — #1b1b1b · 38%
--tanrei-comp-main-action-button-tonal-disabled-state-layer-color — #1b1b1b · 38%
--tanrei-comp-main-action-button-tonal-focused-container-color — #dadada
--tanrei-comp-main-action-button-tonal-focused-container-elevation — 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-main-action-button-tonal-focused-label-color — #262626
--tanrei-comp-main-action-button-tonal-focused-state-layer-color — #262626
--tanrei-comp-main-action-button-tonal-hovered-container-color — #dedede
--tanrei-comp-main-action-button-tonal-hovered-container-elevation — 0px 0px 0px 1px rgb(0 0 0 / 0.08), 0px 1px 1px 0px rgb(0 0 0 / 0.04), 0px 8px 16px -4px rgb(0 0 0 / 0.04), 0px 24px 32px -8px rgb(0 0 0 / 0.06)
--tanrei-comp-main-action-button-tonal-hovered-label-color — #262626
--tanrei-comp-main-action-button-tonal-hovered-state-layer-color — #262626
--tanrei-comp-main-action-button-tonal-label-color sys.color.on-secondary-container #262626
--tanrei-comp-main-action-button-tonal-pressed-container-color — #dadada
--tanrei-comp-main-action-button-tonal-pressed-container-elevation — 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-main-action-button-tonal-pressed-label-color — #262626
--tanrei-comp-main-action-button-tonal-pressed-state-layer-color — #262626
--tanrei-comp-main-action-button-tonal-state-layer-color comp.main-action-button.tonal.label.color #262626
--tanrei-comp-main-action-button-label-typescale-large sys.typescale.heading-5 500 16px/1.5
--tanrei-comp-main-action-button-label-typescale-medium sys.typescale.label 500 14px/1.4286
--tanrei-comp-main-action-button-label-typescale-small sys.typescale.label 500 14px/1.4286

测试样例

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

生成自 specs/main-action-button.spec.json, tokens/comp/main-action-button.tokens.json, design/tokens/

指南

何时使用

主操作按钮用于大多数用户来到这个屏幕要做的那个操作,并且无论他们滚动到哪里,这个操作都应当触手可及:写一条消息、添加一项、开始录音。一个屏幕有一个,或者没有。很多屏幕不需要它:设置页面或阅读视图里,没有哪一个操作值得悬浮在内容之上。

  • 属于某个表单、某个对话框或内容中某一项的操作,请在那些内容所在的位置使用按钮。
  • 针对当前屏幕或所选内容的多个操作,请使用工具栏。
  • 当主操作有几种相关的形式时(新建文档、文件夹或上传),让主操作按钮打开一个列出它们的底部面板。在 FAB 菜单所在的 m3-like 层的系统中,这件事由 FAB 菜单来做;默认系统没有它。
  • 它执行一个操作,不接受目标地址。只把用户带到别处的控件是链接:带 href 的按钮。

展开还是收起

一开始用展开形态。告诉用户这个按钮做什么的,是写出来的名称;只有在用户已经记住图标之后,单独一个图标才靠得住。

当标签的代价大于它的好处时,把它收起:内容在滚动而按钮会遮住内容时,或者在窄窗口里。有空间时再把它展开,例如回到列表顶部时。这个变化由你通过设置 extended 来做;按下按钮绝不会改变它的形态。

  • 形态变化时,按钮的高度和圆角保持不变;哪一条边固定不动,由你的布局决定。锚定在末尾侧时,它朝那条边收起。
  • 没有图标的按钮保持展开,因为没有可以收起成的东西。没有标签的按钮画成收起形态,并会在调试构建中报告:它没有名称。
  • 在减弱动态效果时,形态立即切换。

如何选择变体

变体 强调程度 用于
Filled(默认) 最高 以最强的力度呈现屏幕的主操作。
Tonal 高 应当由内容唱主角的屏幕上的主操作。
Surface 中性 位于本身已有颜色的内容(例如地图或照片)之上的主操作;在那里,带颜色的容器会与内容争抢注意力。

设计系统可以添加自己的变体。

如何选择尺寸

尺寸 默认系统中的高度 用于
Small 40 medium 按钮旁边的第二个相关操作,或密集的布局。
Medium(默认) 56 大多数屏幕的主操作。
Large 96 宽阔或几乎空白的屏幕上的操作;在那里,medium 按钮会被淹没。

高度同时也是收起形态的边长,所以同一个按钮在收起和展开时高度不变。

位置

这个按钮不画遮罩,也不移动任何内容。把它放在内容之上,通常在末尾侧的底角,并避开导航栏、停靠的工具栏和系统的边衬区。在可滚动内容的末尾留出空间,让按钮不会一直遮住最后一项。它的阴影投在容器之外:如果容纳它的容器会裁剪内容,请为阴影留出空间。

全宽

设置 full-width,按钮就和它的容器一样宽,图标和标签居中:在窄屏上横跨屏幕底部的那一个主要操作,例如“去结算”或“发送”。全宽按钮总是展开的:内容滚动时它不会收起,把 extended 设为 false 会在调试构建中报告。它保持自己尺寸的高度、圆角和阴影,放不下的标签会换行。它仍然不自己定位:请把它放进位于屏幕底部的容器里。

全宽的主操作按钮

shopping_cart

推荐

  • 标签写成表示操作的一个动词,或动词加名词:撰写、新建日程、添加照片。
  • 选择用户已经把它和这个操作联系起来的图标,例如表示书写的 edit 或表示创建的 add。
  • 对于指向操作去向的图标,例如“下一步”的箭头,把 iconPosition 设为 trailing。
  • 在每一个有主操作按钮的屏幕上,都把它放在同一个位置。
  • 当它的操作在某个屏幕上不可用时,把它隐藏。这通常比让一个禁用的按钮悬浮在内容之上对用户更好。

避免

  • 不要在一个屏幕上放两个相同尺寸的主操作按钮。如果两个操作看起来同样重要,那么哪一个都不是主操作:请使用内容里的按钮。
  • 不要把它用于次要的、破坏性的或仅用于导航的操作,例如“删除”“设置”或“返回”。
  • 不要把它当作切换按钮使用,也不要通过更改它的标签或图标来显示状态。
  • 不要在用户第一次看到的屏幕上把它收起,那时没有别的东西说明这个图标的含义。
  • 不要让它遮住用户决定是否按下它时所需要的内容。

文案

标签采用句首大写的写法(sentence case),末尾不加标点。以一到两个词为宜。标签不会截断:放得下时保持在一行,布局给它的宽度不够时换行。

无障碍

角色
button
标注
label is the accessible name, whether it is shown or collapsed away. The icon is decorative to assistive technology. A tooltip may show the same text to sighted pointer users while it is collapsed, but it never replaces label.
最小目标
即使外观尺寸更小,点击区域也满足最小目标尺寸。 minimum 32px · pointer 44px · spacing 8px · touch 48px

键盘操作

按键 动作
Tab Moves focus to the main action button.
Enter / Space Activates it.

备注

  • It is an action, never a toggle. Collapsing and extending change what is drawn, not what is announced: the role stays button, the name stays label, and there is no expanded state to report (MAB-20).
  • Its place in the focus and reading order is where the layout puts it in the element tree, not where it is drawn. Put it where its action belongs: after the content it acts on, or first when it is the screen's reason to exist.
  • Extended, text must remain readable at 200% text size: the container grows, the label never truncates. Collapsed, the icon is not text: where sizes are points or dp (SwiftUI, Compose, Flutter, React Native), text size leaves the square and the icon as they are; on the web every Tanrei size follows the root font size (rem), so the square and its icon grow together and stay square and whole. Either way the label is still announced.
  • A trailing icon that points (an arrow) keeps its glyph in right-to-left layouts; the product chooses the glyph that points the right way for the direction.
  • 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/main-action-button.spec.json, tokens/sys/measure.tokens.json

组件负责的部分

  • 在两种形态下,它都是一个以 label 命名的按钮。图标对辅助技术隐藏。
  • 收起和展开改变的是画出来的东西,而不是朗读的内容:角色、名称和焦点都保持原样,不朗读任何内容,也没有“已按下”或“已展开”状态。
  • Enter 和 Space 激活它。禁用时,它不触发任何事件,离开焦点顺序,并暴露为禁用。
  • 在每种尺寸下,包括 small,它的点击区域都达到最小目标。
  • 它只在键盘聚焦时显示焦点指示,不带动画。
  • 展开时,容器随文本大小变大,标签绝不截断。收起时,图标不是文本。在 Web 上,Tanrei 的每个尺寸都跟随根字号,所以正方形和其中的图标一起变大,并保持为正方形。
  • 在从右到左的布局中,图标和标签互换两侧,内边距随之调换。只有图标集标记为表示方向的图标,其本身才会被镜像。
  • 调试构建会报告没有标签的按钮,以及没有图标却被收起的按钮。

需要你做的事

  • 始终用你的产品的语言给它一个 label,即使它永远只以收起形态出现。
  • 把它放在元素树中它的操作所属的位置:在它所作用的内容之后;如果它就是这个屏幕存在的理由,则放在最前面。辅助技术和 Tab 键按树中的顺序到达它,而不是按它画出来的位置。
  • 它收起时,可以考虑为使用指针的用户提供一个文字相同的工具提示。工具提示永远不能代替 label。
  • 确保它不会遮住拥有键盘焦点的内容或控件。在内容的末尾留出空间,或者把按钮移开。
  • 不要依靠变体的颜色来传达含义。请在标签里说出来。