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

概览

Web
tanrei-button-group
schedule
SwiftUI
TanreiButtonGroup
schedule
Jetpack Compose
TanreiButtonGroup
schedule
Flutter
TanreiButtonGroup
schedule
React Native
ButtonGroup
schedule

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

标准式与连接式

取消保存草稿发布format_boldformat_italicformat_underlined

用法

组里的项就是你已经在用的按钮,带标签的或纯图标的都可以,属性和事件也都是按钮自己的。按钮组只负责摆放它们,在连接式(connected)下把它们连接起来,在有标签时为它们命名。

Web 和 React 的示例展示的是当前的规格。SwiftUI、Compose、Flutter 和 React Native 待跟进:它们提供的仍是按钮组的上一版,只有单独一行,没有 orientation、fullWidth、separation 和顺序标志,连接式的项之间总是隔着一条窄间隙,纯图标的项则是另一个独立的图标按钮。在它们跟上之前,这些平台的示例展示的是那一版。

<!-- dismissive 排到一行的起始侧,confirming 排到末尾侧;在一列中两者互换位置。 -->
<tanrei-button-group>
  <tanrei-button variant="outlined" dismissive>取消</tanrei-button>
  <tanrei-button variant="tonal">保存草稿</tanrei-button>
  <tanrei-button variant="filled" confirming>发布</tanrei-button>
</tanrei-button-group>

<!-- 与容器等宽的一列:“允许”画在“以后再说”的上方。 -->
<tanrei-button-group orientation="vertical" full-width>
  <tanrei-button variant="tonal" dismissive>以后再说</tanrei-button>
  <tanrei-button variant="tonal" confirming>允许</tanrei-button>
</tanrei-button-group>

<!-- label 为按钮组命名。每个切换按钮各自保存自己的 selected。 -->
<tanrei-button-group variant="connected" label="文本样式">
  <tanrei-button variant="tonal" toggle label="粗体"><span slot="icon">format_bold</span></tanrei-button>
  <tanrei-button variant="tonal" toggle label="斜体"><span slot="icon">format_italic</span></tanrei-button>
  <tanrei-button variant="tonal" toggle label="下划线"><span slot="icon">format_underlined</span></tanrei-button>
</tanrei-button-group>

<script type="module">
  import '@tanrei/web/components/button-group';
  import '@tanrei/web/components/button';
  // 按钮组自身不触发任何事件:和在别处一样,在按钮上监听。
  document.querySelector('[label="粗体"]').addEventListener('change', (e) => setBold(e.detail.selected));
</script>
import { Button, ButtonGroup } from '@tanrei/react';

// confirming 和 dismissive 是按钮组从子元素上读取的属性。
<ButtonGroup>
  <Button variant="outlined" dismissive onClick={cancel}>取消</Button>
  <Button variant="tonal" onClick={saveDraft}>保存草稿</Button>
  <Button variant="filled" confirming onClick={publish}>发布</Button>
</ButtonGroup>

<ButtonGroup orientation="vertical" fullWidth>
  <Button variant="tonal" dismissive onClick={notNow}>以后再说</Button>
  <Button variant="tonal" confirming onClick={allow}>允许</Button>
</ButtonGroup>

<ButtonGroup variant="connected" label="文本样式">
  <Button variant="tonal" toggle selected={bold} label="粗体" onChange={(e) => setBold(e.detail.selected)}><span slot="icon">format_bold</span></Button>
  <Button variant="tonal" toggle selected={italic} label="斜体" onChange={(e) => setItalic(e.detail.selected)}><span slot="icon">format_italic</span></Button>
</ButtonGroup>
import Tanrei

TanreiButtonGroup {
    TanreiButton("取消", variant: .outlined) { cancel() }
    TanreiButton("保存草稿", variant: .tonal) { saveDraft() }
    TanreiButton("发布") { publish() }
}

TanreiButtonGroup(variant: .connected, label: "文本样式") {
    TanreiIconButton("粗体", isSelected: $bold, variant: .tonal) { Image(systemName: "bold") }
    TanreiIconButton("斜体", isSelected: $italic, variant: .tonal) { Image(systemName: "italic") }
    TanreiIconButton("下划线", isSelected: $underline, variant: .tonal) { Image(systemName: "underline") }
}
import io.github.chnotchy.tanrei.components.TanreiButton
import io.github.chnotchy.tanrei.components.TanreiButtonGroup
import io.github.chnotchy.tanrei.components.TanreiButtonGroupVariant
import io.github.chnotchy.tanrei.components.TanreiIconButton
import io.github.chnotchy.tanrei.primitives.TanreiText
import io.github.chnotchy.tanrei.tokens.TanreiButtonVariant
import io.github.chnotchy.tanrei.tokens.TanreiIconButtonVariant

TanreiButtonGroup {
    TanreiButton(text = "取消", onClick = ::cancel, variant = TanreiButtonVariant.Outlined)
    TanreiButton(text = "保存草稿", onClick = ::saveDraft, variant = TanreiButtonVariant.Tonal)
    TanreiButton(text = "发布", onClick = ::publish)
}

// contentDescription 为按钮组命名。Compose 没有组角色:它是一个遍历组。
TanreiButtonGroup(variant = TanreiButtonGroupVariant.Connected, contentDescription = "文本样式") {
    TanreiIconButton(
        onClick = {},
        contentDescription = "粗体",
        variant = TanreiIconButtonVariant.Tonal,
        checked = bold,
        onCheckedChange = { bold = it },
    ) { TanreiText("format_bold") }
    TanreiIconButton(
        onClick = {},
        contentDescription = "斜体",
        variant = TanreiIconButtonVariant.Tonal,
        checked = italic,
        onCheckedChange = { italic = it },
    ) { TanreiText("format_italic") }
}
import 'package:tanrei/tanrei.dart';

TanreiButtonGroup(
  children: <Widget>[
    TanreiButton(variant: TanreiButtonVariant.outlined, onPressed: cancel, child: const Text('取消')),
    TanreiButton(variant: TanreiButtonVariant.tonal, onPressed: saveDraft, child: const Text('保存草稿')),
    TanreiButton(onPressed: publish, child: const Text('发布')),
  ],
);

TanreiButtonGroup(
  variant: TanreiButtonGroupVariant.connected,
  semanticLabel: '文本样式',
  children: <Widget>[
    TanreiIconButton(
      isSelected: bold,
      onPressed: () => setState(() => bold = !bold),
      variant: TanreiIconButtonVariant.tonal,
      semanticLabel: '粗体',
      icon: const Icon(AppSymbols.formatBold),
    ),
    TanreiIconButton(
      isSelected: italic,
      onPressed: () => setState(() => italic = !italic),
      variant: TanreiIconButtonVariant.tonal,
      semanticLabel: '斜体',
      icon: const Icon(AppSymbols.formatItalic),
    ),
  ],
);
import { Button, ButtonGroup, IconButton } from '@tanrei/react-native';

<ButtonGroup>
  <Button variant="outlined" onPress={cancel}>取消</Button>
  <Button variant="tonal" onPress={saveDraft}>保存草稿</Button>
  <Button onPress={publish}>发布</Button>
</ButtonGroup>

<ButtonGroup variant="connected" accessibilityLabel="文本样式">
  <IconButton variant="tonal" accessibilityLabel="粗体" toggle selected={bold} onSelectedChange={setBold}>
    format_bold
  </IconButton>
  <IconButton variant="tonal" accessibilityLabel="斜体" toggle selected={italic} onSelectedChange={setItalic}>
    format_italic
  </IconButton>
</ButtonGroup>

一行或一列,以及操作的顺序

orientation 决定按钮是排成一行(horizontal,默认)还是叠成一列(vertical);在一列中,每个按钮都与列等宽。fullWidth 让按钮组占满它的容器:一行中的按钮平分宽度,一列中的每个按钮都是这个宽度。面板或狭窄区域底部的操作适合用这种形态。

按钮上的两个标志告诉按钮组它应当放在哪里,这样两种方向下的顺序才能保持一致:

标志 这个按钮 在一行中 在一列中
confirming 完成任务:确定、保存、下一步、删除 在末尾 在顶部
dismissive 退出任务:取消、返回、以后再说 在起始处 在底部
两者都没有 其他任何按钮 在两者之间,按给出的顺序 在两者之间,按给出的顺序

同样的三个按钮,排成一行和一列

取消保存草稿发布取消保存草稿发布

占满宽度:平分宽度的一行,以及一列

以后再说允许
以后再说允许

键盘焦点和屏幕阅读器遵循摆放后的顺序,所以每个人遇到这些操作的顺序都与它们画出来的顺序相同。标志描述的是按钮与任务的关系,而不是语气:在“取消 · 删除”中,“删除”是 confirming 的那一个。破坏性操作靠它自身的外观(按钮的 destructive)和标签来区分,而不是靠位置。对话框和侧边面板也用这两个标志来摆放各自的操作。

以分隔线或间隙连接

连接式按钮组用两种方式之一把按钮连接起来。用分隔线时,按钮彼此相接,相接处的圆角变成直角,每条接缝上有一条线。用间隙时,按钮之间隔着一条窄间隙,彼此相对的圆角收得更小。设计系统为所有连接部件的组件统一选择一次(默认系统画分隔线),设计系统也可以让按钮组单独不同,而 separation(auto、divider、gap)则设置某一个按钮组。标准式按钮组会忽略它。

以分隔线和以间隙连接,排成一行和一列

剪切复制粘贴剪切复制粘贴addremoveaddremove

使用分隔线时,只有两侧按钮的填充相同,接缝上才有线。在选中的切换按钮与未选中的切换按钮之间,或 filled 按钮与 outlined 按钮之间,填充的边缘已经标出了接缝,所以那里不画线。

以分隔线连接的切换按钮:选中项的两旁没有线

format_boldformat_italicformat_underlinedformat_strikethrough

规格

连接式:outlined、large,以及含有禁用项

剪切复制粘贴上一个今天下一个剪切复制粘贴

构成

部件 说明
1 group The row or column that holds the Buttons. It places and spaces the Buttons, joins them in the connected variant, and names them as a group. It draws nothing of its own but the dividers of a connected group.
2 button A Tanrei Button placed in the group: icon-only, label-only or both, its icon on either side, a toggle or not. It is drawn from its own tokens and keeps its own states, focus, events and semantics.
3 inner-corner · 可选 In the connected variant, a corner of a Button that faces its neighbour. The group sets it.
4 outer-corner · 可选 In the connected variant, a corner at an end of the joined shape: the start corners of the first Button and the end corners of the last in a row, the top corners of the first and the bottom corners of the last in a column. The group sets it.
5 divider · 可选 In the connected variant under divider, the line on the seam between two Buttons that touch. The group draws it; under gap there is none.

属性

属性 类型 默认值 说明
variant standard | connected "standard" standard spaces the Buttons apart. connected joins them into one shape with rounded outer ends, by a divider or by a narrow gap (separation), for a closely related set (Bold, Italic, Underline; Previous, Today, Next).
orientation horizontal | vertical "horizontal" horizontal lays the Buttons out in one row; vertical stacks them in one column, every Button as wide as the column. The confirming and dismissive Buttons change places with the orientation (BGR-13, BGR-14).
separation auto | divider | gap "auto" How the Buttons of a connected group are joined. divider: they touch, and a line is drawn on each seam. gap: they stand a narrow gap apart, with tighter corners where they face each other. Auto takes comp.button-group.connected.separation, the design system's answer (divider in the default system). Ignored while variant is standard.
fullWidth boolean false The group fills its container's width and its Buttons share it equally (in a column, each is the container's width). For the actions at the foot of a dialog, a picker or a side sheet.
label string "" The accessible name of the group ("Text style"). Optional: a group of actions that are each named by their own label needs none.

条目

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

button

A Button in the group: the button component, placed, spaced and (when connected) given its corners and minimum size by the group, otherwise drawn and behaving as it does anywhere.

属性 类型 默认值 说明
variant filled | tonal | elevated | outlined | text "filled" Its emphasis, as the Button's variant.
size small | medium | large "medium" Its height, as the Button's size. Give every Button of a group the same size.
width default | narrow | wide "default" An icon-only Button's width, as the Button's width. A connected group still widens it to its minimum (BGR-3).
icon string "" The glyph in the Button's icon slot, by its name in the system's icon set. Empty for a label-only Button. It is how a fixture fills that slot; in an application the icon slot is filled as the Button's own spec says.
iconPosition leading | trailing "leading" Which side of the label the icon is on, as the Button's iconPosition.
label string "" Its accessible name, as the Button's label prop. Required for an icon-only Button.
toggle boolean false As the Button's toggle.
selected boolean false As the Button's selected.
disabled boolean false As the Button's disabled.
confirming boolean false The Button completes the task (OK, Save, Next, Delete). The group places it at the end of a row and at the top of a column (BGR-13, BGR-14). It changes the Button's place only, never its look (BGR-19).
dismissive boolean false The Button backs out of the task (Cancel, Back, Not now). The group places it at the start of a row and at the bottom of a column (BGR-13, BGR-14). It changes the Button's place only, never its look (BGR-19).
插槽 说明
label Its label, as the Button's default slot. A fixture item's content is this label; an icon-only item has none.
icon Its glyph, as the Button's icon slot. A fixture fills it through the icon prop.
事件 说明
click The Button was activated, as the Button's click.
change A toggle Button was switched, as the Button's change.
规格 Web SwiftUI Jetpack Compose Flutter React Native
条目 tanrei-button TanreiButton TanreiButton TanreiButton Button
variant · 属性 variant variant variant variant variant
size · 属性 size size size size size
width · 属性 width width width width width
icon · 属性 slot="icon" icon icon icon icon
iconPosition · 属性 iconPosition iconPosition iconPosition iconAlignment iconPosition
label · 属性 label label contentDescription semanticLabel accessibilityLabel
toggle · 属性 toggle isSelected onCheckedChange isSelected toggle
selected · 属性 selected isSelected checked isSelected selected
disabled · 属性 disabled disabled enabled onPressed disabled
confirming · 属性 confirming tanreiGroupPlacement(.confirming) Modifier.confirming() TanreiButtonGroupItem.placement confirming
dismissive · 属性 dismissive tanreiGroupPlacement(.dismissive) Modifier.dismissive() TanreiButtonGroupItem.placement dismissive
click · 事件 click action onClick onPressed onPress
change · 事件 change isSelected onCheckedChange onPressed onSelectedChange
备注 A tanrei-button child, as anywhere: its label is its content and its glyph a tanrei-icon in its icon slot. confirming and dismissive are boolean attributes on the child that the group reads (a MutationObserver re-places the items when they change, BGR-11); the Button itself draws nothing for them. A TanreiButton child, as anywhere. A toggle is a Button given an isSelected binding. The two flags are one modifier, .tanreiGroupPlacement(.confirming) or (.dismissive), a LayoutValueKey the group's layout reads, as .layoutPriority is read by a stack; one enum cannot be both (BGR-18 cannot arise). A TanreiButton child, as anywhere; a toggle is a Button given checked and onCheckedChange, and enabled is disabled's inverse. The two flags are modifiers of the group's scope, Modifier.confirming() and Modifier.dismissive(), parent data the group's layout reads, as RowScope.weight is. A TanreiButton child, as anywhere; a null onPressed disables it, and a non-null isSelected makes it a toggle. The two flags are one ParentDataWidget, TanreiButtonGroupItem(placement: TanreiGroupPlacement.confirming | .dismissive, child:), as Flexible is for a Flex; one enum cannot be both (BGR-18 cannot arise). A Button child, as anywhere. confirming and dismissive are props on the child that the group reads from its children; the Button itself draws nothing for them.

状态

行为

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

ID 需求
BGR-1 WHILE orientation is horizontal THE button group SHALL lay out its items in one row, in the order BGR-13 gives, each at its own size and width unless fullWidth is true (BGR-17): WHILE variant is standard comp.button-group.standard.gap apart and centred on the row's middle; WHILE variant is connected joined as its separation names (BGR-21) — comp.button-group.connected.gap apart under gap, touching under divider — and each stretched to the height of the tallest item (a Button's height is a minimum), so that the joined shape stays even when a label wraps or text is enlarged.
BGR-2 WHILE variant is connected and orientation is horizontal THE button group SHALL give the start corners of the first item and the end corners of the last the radius comp.button-group.connected.horizontal.outer-shape, and each corner that faces a neighbouring item the radius comp.button-group.connected.inner-shape WHILE its separation is gap and no radius, a square corner, WHILE its separation is divider (BGR-21), each capped at half the item's height and eased by sys.shape.corner-smoothing; the item's fill, outline, state layer, ripple and focus ring SHALL follow the resulting shape. WHILE variant is standard THE button group SHALL leave every corner of every item as the item draws it.
BGR-3 WHILE variant is connected THE button group SHALL make each item at least sys.target.touch for touch input and sys.target.pointer for fine pointers along the direction in which it meets its neighbours — wide in a row, high in a column — because joined items cannot extend a hit area over each other: an icon-only Button narrower than that widens its container around its centred icon, and a Button lower than that in a column grows its container around its centred content. Across the other direction the items SHALL extend their hit areas as BTN-5 says. A platform that cannot tell the input type SHALL use sys.target.touch.
BGR-4 WHILE variant is standard and the hit areas that two neighbouring items extend (BTN-5) overlap THE button group SHALL give every press on an item's visible bounds to that item, and a press in the gap between two items to the nearer one, so that neighbouring hit areas meet at the middle of the gap. comp.button-group.standard.gap is what keeps each item's target whole there: at sys.target.touch minus an item's width (in a row) or height (in a column) or more, no hit areas overlap and every item keeps its full target between its neighbours, as the default system's gap does for every Button size; a layer that narrows the gap narrows the targets between items.
BGR-5 THE button group SHALL NOT restyle its items beyond BGR-2, BGR-3, BGR-12, BGR-16, BGR-17, BGR-22 and BGR-23: each item SHALL render from its own tokens, answer hover, focus and press itself, switch itself WHILE its toggle is true, emit its own events, keep its own semantics and be its own Tab stop in the order BGR-15 gives; the group itself SHALL NOT take focus, SHALL NOT handle arrow keys and SHALL keep no selection of its own.
BGR-6 WHILE label is not empty THE button group SHALL expose itself to assistive technology as a group named by label that contains its items; WHILE label is empty THE button group SHALL expose no element of its own, only its items, unless the product names it (BGR-25).
BGR-7 WHERE orientation is horizontal, fullWidth is false and the row is wider than the space its layout gives it THE button group SHALL narrow its labelled Buttons in proportion to their widths, down to the width of their longest word, letting their labels wrap as a Button's label does when its layout gives it less width, and SHALL keep icon-only Buttons at their width; it SHALL NOT wrap the row onto a second line or scroll it, and in debug builds SHALL report a row that still does not fit.
BGR-8 WHEN the layout direction is right-to-left THE button group SHALL, in a row, put its start at the right — the dismissive items at the right and the confirming items at the left (BGR-13), the first placed item's outer corners on its right — and mirror which corners are inner corners; in a column it SHALL keep the top-to-bottom order of BGR-14. Each item's content SHALL follow the layout direction as the Button's does.
BGR-9 WHEN variant, orientation or separation names a value the button group does not define THE button group SHALL render with that prop's default (standard, horizontal, auto), and in debug builds report the unknown value.
BGR-10 WHILE the group holds fewer than two items, or content that is not a Tanrei Button, THE button group SHALL still render it, a single item keeping every corner as it draws it, and in debug builds report it; a platform that cannot inspect its content SHALL state the rule in its README and docs page.
BGR-11 WHEN an item becomes disabled, is added or is removed, or its confirming or dismissive changes, THE button group SHALL place its items again by BGR-13 and BGR-14 and recompute which corners are inner and outer corners, where its dividers are (BGR-24) and which items carry the outer padding of BGR-23, at once, without animation; a disabled item SHALL keep its place, its gap or its dividers, and its corners, and an item that has focus while it moves SHALL keep it.
BGR-12 WHILE orientation is vertical THE button group SHALL lay out its items in one column, top to bottom in the order BGR-14 gives, comp.button-group.standard.gap apart WHILE variant is standard and, WHILE variant is connected, comp.button-group.connected.gap apart under gap and touching under divider (BGR-21), and SHALL make every item as wide as the column — the widest item's width, or the container's width WHILE fullWidth is true — its content centred as a Button centres it in a wider container. WHERE the column is given less width than its widest item every item SHALL take that width and its label SHALL wrap as BGR-7 says; the column SHALL NOT scroll.
BGR-13 WHILE orientation is horizontal THE button group SHALL place its items from start to end in three runs: first the items whose dismissive is true, then the items with neither flag, then the items whose confirming is true; within each run the items SHALL keep the order they were given in. The start is the left in a left-to-right layout and the right in a right-to-left one (BGR-8).
BGR-14 WHILE orientation is vertical THE button group SHALL place its items from top to bottom in three runs: first the items whose confirming is true, then the items with neither flag, then the items whose dismissive is true; within each run the items SHALL keep the order they were given in, the first on top, in either layout direction.
BGR-15 THE button group SHALL make the focus order, and the order in which assistive technology reads its items, the placed order of BGR-13 and BGR-14 — start to end in a row, top to bottom in a column — whatever order the items were given in; a platform whose visual reordering the focus order does not follow (the web's CSS order) SHALL reorder the items themselves in the tree that focus and assistive technology walk.
BGR-16 WHILE variant is connected and orientation is vertical THE button group SHALL give the top corners of the first item and the bottom corners of the last the radius comp.button-group.connected.vertical.outer-shape, and every other corner of every item comp.button-group.connected.inner-shape WHILE its separation is gap and no radius, a square corner, WHILE its separation is divider (BGR-21), each capped at half the item's height and eased by sys.shape.corner-smoothing; the item's fill, outline, state layer, ripple and focus ring SHALL follow the resulting shape.
BGR-17 WHILE fullWidth is true and orientation is horizontal THE button group SHALL take the whole width its container gives it and divide it, less the gaps (none under divider), into equal shares, one per item, whatever each item holds: a label shorter than its share SHALL be centred in it, a label longer SHALL wrap as BGR-7 says, and an icon-only Button SHALL widen its container around its centred icon. WHERE a share is narrower than an item's minimum width (BGR-3, or an icon-only Button's own width) THE button group SHALL give that item its minimum, share the rest equally among the others, and in debug builds report it. WHILE fullWidth is false THE button group SHALL take its natural width.
BGR-18 IF an item's confirming and dismissive are both true THE button group SHALL place it among the items with neither flag and in debug builds report it.
BGR-19 THE button group SHALL NOT change an item's look, emphasis or semantics for its confirming or dismissive: the flags decide its place only. A destructive confirming action (Delete) takes its look from its own variant and its own destructive (BTN-40), and a Button is not made filled, or destructive, by being confirming.
BGR-20 THE button group 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 button group is shown THE button group SHALL draw itself for the new direction at once, without a motion of its own, keeping what it shows and its state.
BGR-21 WHILE variant is connected THE button group SHALL join its items as its separation names: WHILE separation is auto, as comp.button-group.connected.separation names, which follows sys.behavior.separation unless a design system sets it apart; WHILE separation is divider or gap, as that value names, whatever the tokens say. Under gap the items SHALL stand comp.button-group.connected.gap apart with nothing drawn between them and the inner corners of BGR-2 and BGR-16; under divider the items SHALL touch, with square inner corners and the divider of BGR-22 on each seam. WHILE variant is standard THE button group SHALL ignore separation and space its items by comp.button-group.standard.gap. WHEN separation, or the token it follows, changes THE button group SHALL draw the other form at once, without animation, keeping the order, the focus and the state of its items. A platform that cannot draw a choice SHALL fall back to the first value, divider, and state that in its README.
BGR-22 WHILE variant is connected and its separation is divider THE button group SHALL draw, on each seam between two neighbouring items that BGR-24 says carries one, a line comp.button-group.connected.divider.thickness thick, in the color BGR-24 names (comp.button-group.connected.divider.color, or comp.button-group.connected.divider.on-fill.color between two emphasis fills), centred on the seam, over the items' fills and state layers, running along the seam — top to bottom in a row, start to end in a column — and stopping comp.button-group.connected.divider.inset short of each end of it. The line SHALL take no space of its own, SHALL NOT take a press (a press on it goes to the item under the point), SHALL NOT be exposed to assistive technology and SHALL keep its color whatever state its neighbours are in. Each item SHALL draw neither its outline nor the ring of its elevation along an edge that meets a seam, whether or not a line is drawn on it, so that the divider is the only line a seam can carry and the outline of an outlined group runs once around the joined shape, in the color of its rules in the default system; an item's state layer, ripple and focus ring SHALL still follow that item's own shape.
BGR-23 WHILE variant is connected and orientation is horizontal THE button group SHALL add comp.button-group.connected.horizontal.outer-inset to the inline padding of the first item at its start and of the last item at its end, under either separation, so that content beside a rounded outer end looks as far from it as from the tighter corner on its other side: a labelled Button SHALL be wider by that amount with its content moved inward by it, and an icon-only Button SHALL be wider by that amount with its glyph centred in what is left, half of it nearer its neighbour. A single item (BGR-10) SHALL get none; a column SHALL add none, its ends not being full (BGR-16); WHILE fullWidth is true the shares of BGR-17 SHALL be divided first and the padding taken inside the end items' shares. In a right-to-left layout the start is the right, as BGR-8 says.
BGR-24 WHILE variant is connected and its separation is divider THE button group SHALL draw the line of BGR-22 on a seam only WHILE the two items it joins rest on the same fill: the colors from which they render their containers at rest — comp.button.outlined.container.color for an outlined Button, and so on for each variant, and for a toggle the selected or the unselected part of its variant (BTN-15) — are the same color in the active scheme, two containers that have no fill counting as the same. WHILE the two fills differ (a selected toggle beside an unselected one, a filled Button beside an outlined one) THE button group SHALL draw no line on that seam, because the edge of the fill already marks it, and the two items SHALL still draw neither outline nor elevation ring along it (BGR-22). A toggle of the text variant has no fill, selected or not, so its seams keep their lines. THE button group SHALL draw the line in comp.button-group.connected.divider.color, and in comp.button-group.connected.divider.on-fill.color WHILE both items rest on an emphasis fill: a filled Button that is not a toggle, or a selected toggle of the filled, tonal, elevated or outlined variant. It SHALL compare the fills the items have at rest: hover, focus and press SHALL neither add nor remove a line nor change its color, and a disabled item SHALL be compared by the look it has when enabled, so that disabling an item moves no line (BGR-11). WHEN an item's selected, toggle or variant changes THE button group SHALL add, remove or recolor the lines beside it at once, without animation.
BGR-25 WHERE the platform has ARIA attributes (the web), the host carries its group role (BGR-6) itself, so what the product writes on it is not moved, but for a name: WHILE label is not empty THE button group SHALL hold the product's aria-label and aria-labelledby on that host as their data-aria- twins, so that label is its whole name although the platform gives aria-labelledby the precedence, and WHEN label is emptied SHALL give them back as they were written; WHILE label is empty a name the product gives so — aria-label, or aria-labelledby naming an element — SHALL be its name. WHILE label is empty and the product names it so THE button group SHALL be exposed as a group named by that name, as with a label: a group the product names is a named group, and a name on an element with no role would name nothing. WHEN the product removes one, or its data-aria- twin, or sets the ARIA property that reflects it (ariaLabel, ariaLabelledByElements) to null, THE button group SHALL take it away.

各平台名称对照

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

规格 Web SwiftUI Jetpack Compose Flutter React Native
组件 tanrei-button-group TanreiButtonGroup TanreiButtonGroup TanreiButtonGroup ButtonGroup
variant · 属性 variant variant variant variant variant
orientation · 属性 orientation orientation orientation orientation orientation
separation · 属性 separation separation separation separation separation
fullWidth · 属性 fullWidth fullWidth fullWidth fullWidth fullWidth
label · 属性 label label contentDescription semanticLabel accessibilityLabel
备注 <tanrei-button-group variant="connected" orientation="vertical" full-width label="Text style"> holds tanrei-button children. It places them without moving them in the author's DOM: its shadow root has three slots in the order of the orientation (dismissive, unflagged, confirming in a row; confirming, unflagged, dismissive in a column) and assigns each child to one with manual slot assignment, so focus and the accessibility tree, which walk the flat tree, follow the placed order (BGR-15) and CSS order is not used. It sets role group and aria-label on its own element only while label is not empty (ElementInternals is not used for the role: axe-core does not see semantics set through it). It gives a connected item its corners and minimum size through CSS custom properties that Button reads on its container (--tanrei-group-start-radius, --tanrei-group-end-radius, and in a column the top and bottom radii, --tanrei-group-min-width, --tanrei-group-min-height); these are an internal contract between Tanrei elements, not a styling API. Each item also learns its place in the group through an internal controller, because three things cannot be done with custom properties alone: a label that may wrap (BGR-7, BGR-17), corners that never animate and pill ends held to half the measured height (BGR-11), and a hit area that stops at half the gap (BGR-4). It reports other children in debug builds (BGR-10). separation is an attribute (auto, divider, gap); auto reads the name in --tanrei-comp-button-group-connected-separation. Under divider the group draws each rule itself, in its shadow root over the seam, and tells each item through the same internal controller which of its edges meet a seam (no outline, no ring) and how much padding to add at an outer end (BGR-22, BGR-23). Owed on the web: orientation="vertical" must really stack the items (BGR-12, BGR-14, BGR-16) — today the element and the review gallery draw every vertical fixture as a row — and its fixtures must be drawn as columns in the gallery and the baselines. Lagging: ships an earlier revision, a row of Buttons and the retired icon-only kind with no orientation, placement flags or full width. The target: TanreiButtonGroup(variant:, orientation:, fullWidth:, label:) { TanreiButton … }, a custom Layout that places its subviews by BGR-13 and BGR-14 and gives them .accessibilitySortPriority in that order so VoiceOver and keyboard focus follow it (BGR-15). The group passes a connected item its corners and minimum size through an environment value that TanreiButton reads (internal to the package). With a label it is .accessibilityElement(children: .contain) with .accessibilityLabel(label). The group reads its content through a variadic view, as the menu and tabs do, so it reports fewer than two items and content that is not a Tanrei Button in debug builds (BGR-10). Also owed: separation (auto, divider, gap), the divider of BGR-22, the outer padding of BGR-23 and the nine new fixtures; reading comp.button-group.connected.separation as a typed choice needs the native token emitters to write a component's choice of kind as a typed enumeration, which they do not yet. Lagging: ships an earlier revision, a row of Buttons and the retired icon-only kind with no orientation, placement flags or full width. The target: TanreiButtonGroup(variant, orientation, fullWidth, contentDescription) { TanreiButton(…) }, a custom Layout whose items read a CompositionLocal for their corners and minimum size (internal to the library). It places the items by BGR-13 and BGR-14 and sets traversalIndex in that order so TalkBack and focus follow it (BGR-15). With a content description it is semantics { isTraversalGroup = true; contentDescription = … }; Compose has no group role. Debug builds report content that is not a Tanrei Button, and a group of fewer than two items (BGR-10). Also owed: separation (auto, divider, gap), the divider of BGR-22, the outer padding of BGR-23 and the nine new fixtures; reading comp.button-group.connected.separation as a typed choice needs the native token emitters to write a component's choice of kind as a typed enumeration, which they do not yet. Lagging: ships an earlier revision, a row of Buttons and the retired icon-only kind with no orientation, placement flags or full width. The target: TanreiButtonGroup(variant:, orientation:, fullWidth:, semanticLabel:, children: [TanreiButton(…)]), a MultiChildRenderObjectWidget that places its children by BGR-13 and BGR-14 and wraps each in a FocusTraversalOrder and Semantics(sortKey:) in that order (BGR-15); the items read an InheritedWidget for their corners and minimum size (internal to the package). With a label it is Semantics(container: true, label:, explicitChildNodes: true). Children of another type are reported in debug builds (BGR-10). Also owed: separation (auto, divider, gap), the divider of BGR-22, the outer padding of BGR-23 and the nine new fixtures; reading comp.button-group.connected.separation as a typed choice needs the native token emitters to write a component's choice of kind as a typed enumeration, which they do not yet. Lagging: ships an earlier revision, a row of Buttons and the retired icon-only kind with no orientation, placement flags or full width. The target: <ButtonGroup variant orientation fullWidth accessibilityLabel>{…}</ButtonGroup> with Button children, which it renders in the order of BGR-13 and BGR-14 (reordering the children themselves, so focus and the screen reader follow it, BGR-15) and which read a React context for their corners and minimum size (internal to the package). With a label it has role="group" and accessibilityLabel; without one it adds no accessibility element. Children of another type are reported in development builds (BGR-10). Also owed: separation (auto, divider, gap), the divider of BGR-22, the outer padding of BGR-23 and the nine new fixtures; reading comp.button-group.connected.separation as a typed choice needs the native token emitters to write a component's choice of kind as a typed enumeration, which they do not yet.

令牌

令牌 引用 解析值(浅色)
--tanrei-comp-button-group-connected-divider-color sys.color.outline-variant #000000 · 8%
--tanrei-comp-button-group-connected-divider-inset sys.space.space100 8px
--tanrei-comp-button-group-connected-divider-on-fill-color sys.color.outline #848484
--tanrei-comp-button-group-connected-divider-thickness sys.border.hairline 1px
--tanrei-comp-button-group-connected-gap sys.space.space25 2px
--tanrei-comp-button-group-connected-horizontal-outer-inset sys.space.space50 4px
--tanrei-comp-button-group-connected-horizontal-outer-shape sys.shape.corner.full 9999px
--tanrei-comp-button-group-connected-inner-shape sys.shape.corner.small 6px
--tanrei-comp-button-group-connected-separation sys.behavior.separation 0
--tanrei-comp-button-group-connected-vertical-outer-shape sys.shape.corner.large 12px
--tanrei-comp-button-group-standard-gap — 16px

测试样例

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

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

指南

何时使用

当几个按钮属于一个整体时,使用按钮组:表单的末尾(取消、保存草稿、发布),编辑器的格式设置(粗体、斜体、下划线),日历的“上一个”“今天”和“下一个”。按钮组确定它们的间距,在它们是一组时把它们连接起来,在有帮助时为它们命名。其中的每个按钮仍然是按钮。

针对当前屏幕或所选内容的一行操作,如果整体是一个 Tab 停靠点、用方向键在其中移动,那就是工具栏。一个主操作与列出其变化形式的菜单连在一起,是带菜单的按钮:它看起来像由两个按钮组成的连接式按钮组,但它的后半边总是打开菜单。

分段按钮、按钮组、纸片(Chip)还是标签页

这四种组件都把几个相关的东西排成一行,所以容易混淆。它们的区别在于按下之后发生什么,以及键盘如何在其中移动。

组件 它是什么 选择 键盘 用于
分段按钮(Material 和 cupertino 预设提供;Tanrei 系统中没有) 一个控件:两到五个连在一起的选项,回答同一个问题。 由它自己保存,以值表示:一个选项,或多个。 一个 Tab 停靠点。方向键在选项之间移动,单选时移动的同时也选中。 只有几个固定答案、立即生效的设置:日、周、月;左对齐、居中、右对齐。
按钮组 由各自独立的按钮排成的一行或一列,彼此隔开或连接成一个形状。 没有。组里的切换按钮各自保存自己的开关状态。 每个按钮都是一个单独的 Tab 停靠点。 相关的操作:取消、保存草稿、发布;作为相互独立的切换按钮的粗体、斜体和下划线。
纸片 排成一行的、各自独立的小控件,这一行可以换行或滚动。 每个筛选纸片各自开或关。 每个纸片都是一个单独的 Tab 停靠点。 列表上方的筛选条件、建议的回复、用户输入的条目。
标签页 同一主题的几个视图,每个视图有自己的面板。 一个标签页:屏幕上显示的那个视图。 一个 Tab 停靠点。方向键移动并选中。 切换显示哪些内容:概览、动态、设置。

一个快速的判断方法:如果按下它会执行某件事,那是按钮组。如果它改变下方的内容,那是标签页。如果它设置一个值,那是单选按钮;在提供分段按钮的系统中,也可以是分段按钮。选项很多或相互独立时,用纸片。

如何选择变体

变体 作用 用于
Standard(默认) 把按钮彼此隔开,间距足以让每个按钮在相邻按钮之间保有完整的触摸目标。每个按钮保留自己的圆角。 放在一起但各自独立的操作,例如表单的“取消”和“保存”。
Connected 用分隔线或窄间隙把按钮连接成一个外端呈圆形的形状(参见以分隔线或间隙连接)。每个按钮的宽度至少等于一个目标,并拉伸到与最高的按钮同高。 读起来像一个控件的、关系紧密的一组:粗体、斜体、下划线;上一个、今天、下一个。

在连接式按钮组中,只有面向相邻按钮的圆角会改变:用分隔线时是直角,用间隙时收得更小。填充、轮廓、状态层、涟漪和焦点环都沿着连接后的形状绘制;连成一行时,圆形的外端会多一点内边距,让两端按钮的内容看起来留白均匀。small 的纯图标按钮会以图标为中心加宽到目标宽度,因为连在一起的按钮无法把点击区域伸到彼此之上。

一行还是一列

默认用一行。当按钮带着完整的标签无法并排放下时(例如窄面板或手机宽度的区域里的操作),或者放在纵向排列的内容旁边时(例如地图的缩放控件),用一列。请标出完成任务的按钮和退出任务的按钮,这样布局在一行和一列之间变化时,顺序仍然是用户预期的那一个。

推荐

  • 让组里的每个按钮都使用相同的尺寸。
  • 和在别处一样分配强调:最多一个 filled 按钮,通常放在末尾。
  • 当按钮只有放在一起才有意义时,给按钮组加上标签,例如“文本样式”。如果一行操作各自都说明了自己做什么,就不用加。
  • 连接式按钮组里的按钮使用同一种变体,这样连接后的形状读起来才是一个整体。
  • 把完成任务的按钮标为 confirming,把退出任务的按钮标为 dismissive,让按钮组来摆放它们。书写顺序随意;按钮组不会在你的标记中移动它们。
  • 除非某个按钮组必须不同,否则让 separation 保持 auto,这样产品中所有连接式控件的分隔方式都一致。

避免

  • 不要用组里的切换按钮来表示相互排斥的选择,例如左对齐、居中和右对齐。按钮组不会让其中的切换按钮互斥。选择会改变下方内容时用标签页,设置一个值时用单选按钮,在提供分段按钮的系统中也可以用分段按钮。
  • 除了 Tanrei 的 Button(带标签的或纯图标的)之外,不要把任何东西放进按钮组:输入框、纸片、菜单都不行。其他内容会在调试构建中报告。
  • 不要用标志来表达语气。完成任务的破坏性操作“删除”是 confirming,外观取自它自己的 destructive;同时带有两个标志的按钮会按两者都没有来摆放,并被报告。
  • 不要指望一行会换行。放不下时,这一行会让其中的 Button 变窄,并让它们的标签换行。它绝不会移到第二行,也不会滚动。
  • 不要在一个按钮组里混用尺寸。按钮组不会调整按钮的尺寸。

无障碍

角色
group
标注
label, when given, names the group ("Text style"). Each item is named as it is anywhere: a Button by its label text, an icon-only Button by its label prop.
最小目标
即使外观尺寸更小,点击区域也满足最小目标尺寸。 minimum 32px · pointer 44px · spacing 8px · touch 48px

键盘操作

按键 动作
Tab / Shift+Tab Moves focus to each enabled item in turn in the placed order (BGR-15): start to end in a row, top to bottom in a column.
Enter / Space Activates the focused item, as the Button itself says (a toggle switches).

备注

  • A button group is a layout of actions: each item is its own Tab stop with its own semantics. A row that should be one Tab stop with arrow keys is a toolbar; one choice among several is a single-selection control: tabs where it changes the content below, a radio group where it sets a value, or a segmented button in a system that ships one.
  • A toggle Button in a group keeps its own on/off state; the group does not make its toggles exclusive. One choice among several is a single-selection control: tabs where it changes the content below, a radio group where it sets a value, or a segmented button in a system that ships one.
  • confirming and dismissive are not announced: they decide where a Button is, and focus and reading follow that place, so a screen-reader user meets the actions in the same order a sighted user sees them.
  • The two flags describe the task, not the tone: in "Cancel · Delete" Delete is confirming. A destructive action is told apart by its own look (the Button's destructive) and its label, not by its place.
  • Compose has no group role: the group is a traversal group whose content description is label, and each item keeps its own role.
  • Text size: Buttons in a group grow as they do anywhere; a connected row stretches every item to the tallest, and a column makes every item as wide as the widest.
  • A divider is decoration: it is not exposed, takes no press and is no boundary a person must find. What tells two joined Buttons apart for assistive technology is that each is its own button with its own name; for sight, their content and the line or the gap between them.
  • A seam between a selected and an unselected toggle carries no line: the fill of the selected item ends there, at a contrast far above 3:1 against its neighbour, and that edge is the boundary. The selected state itself is exposed by the toggle (BTN-14), never by the line.
  • In the default system the outline of an outlined group and its rules are the hairline (the divider's color), far below 3:1 against the surface. That is within WCAG 1.4.11: each Button is identified by its label or its icon, not by its edge.

生成自 specs/button-group.spec.json, tokens/sys/measure.tokens.json

组件负责的部分

  • 每个按钮都保有自己的角色、名称、状态和 Tab 停靠点。Tab 按摆放后的顺序在它们之间移动:一行中从起始侧到末尾侧,一列中从上到下。按钮组自身不接收焦点,不处理方向键,也不保存任何选择。
  • confirming 和 dismissive 不会被朗读。它们决定按钮的位置,焦点和朗读都遵循这个位置。
  • 分隔线是装饰:它对辅助技术隐藏,也不接收按下。让人区分两个相连按钮的,是它们各自是一个按钮、各有自己的名称。
  • 有标签时,按钮组作为一个以该标签命名、包含这些按钮的组暴露给辅助技术。没有标签时,它不给无障碍树增加任何东西。
  • 在连接式按钮组中,每个按钮的宽度至少等于一个目标,点击区域向这一行的上方和下方延伸到最小目标。在标准式按钮组中,按在某个按钮可见范围内的按下总是交给这个按钮,按在间隙里的则交给较近的那个按钮。
  • 焦点指示沿着每个按钮的形状绘制,包括连接处的圆角。
  • 文本放大时,按钮和在别处一样变大。连接式的一行把每个按钮拉伸到与最高的按钮同高,一列让每个按钮与最宽的按钮同宽,所以连接后的形状保持整齐。
  • 在默认系统中,outlined 按钮组的轮廓及其分隔线都是细线,有意画得很淡:让人认出每个按钮的是它的标签或图标,而不是它的边线。
  • 调试构建会报告以下情况:按钮组少于两项,内容不是 Tanrei 的 Button,按钮同时带有两个标志,变体、方向或分隔方式未知,占满宽度的一行中按钮无法平分宽度,以及一行在其中的 Button 变窄之后仍然放不下。

已知的平台差距:

  • Web:连接式按钮组中的图标按钮,在鼠标等精确指针下宽度等于指针目标,在粗略指针下宽度等于触摸目标。当某一项自身的形状是胶囊形时,它转向相邻项的圆角是圆弧,而不是经过圆角平滑处理的曲线。在按钮组内,Button 的标签可以换行;在按钮组外,它保持在一行。一行仍然放不下的报告是在按钮组尺寸变化或其子元素变化时检查的,而不是在某一项的标签变化时。
  • SwiftUI 没有组角色。带标签的按钮组是一个以其标签命名、容纳这些按钮的容器。标题为 LocalizedStringKey 的 Button(字符串字面量就是这样)可能被压缩到比它最长的单词还窄,这时会在单词内部断行。一行仍然放不下的报告在调试构建中是断言失败,所以哪怕布局只是一时过窄,例如宽度带动画的容器,也会让调试中的应用停止。
  • Compose 没有组角色。带标签的按钮组是一个以该标签作为内容描述的遍历组,每个按钮保有自己的角色。
  • React Native:iOS 和 Android 如何分配按在两个按钮之间间隙里的按下,文本放大时连接式按钮组中被拉伸的按钮,以及焦点进入按钮组时 VoiceOver 和 TalkBack 是否朗读按钮组的名称,都还没有在真机上检查过。
  • React Native:变窄的 Button 可能缩到比它最长的单词还窄,这个单词的字母于是被拆到多行,因为 React Native 的布局没有文本的最小内容宽度。
  • 在标准式按钮组中,相邻的点击区域在间隙的中点相接。默认系统的间隙(16)足够宽,每个图标按钮(包括 small 的)都能在相邻按钮之间保有完整的 48 触摸目标。间隙更窄的设计系统会让按钮之间的目标变窄。

需要你做的事

  • 为每个图标按钮命名。按钮组的标签不会为其中的按钮命名。
  • 把按钮组放在阅读顺序中它的按钮应处的位置。辅助技术按它们出现的顺序逐个到达。
  • 如果一组切换按钮必须保持互斥,请使用单选的控件:选择会改变下方内容时用标签页,设置一个值时用单选按钮,在提供分段按钮的系统中也可以用分段按钮。它们各自都会作为“选择其一”被朗读;一行切换按钮则不会。