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

概览

Web
tanrei-segmented-button
schedule
SwiftUI
TanreiSegmentedButton
schedule
Jetpack Compose
TanreiSegmentedButton
schedule
Flutter
TanreiSegmentedButton
schedule
React Native
SegmentedButton
schedule

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

并非每个设计系统都有

cupertino, m3-expressive-like, m3-like 提供此组件,由 @tanrei/web/cupertino, @tanrei/web/m3-expressive-like, @tanrei/web/m3-like 注册。

tanrei 中没有此组件。 One choice among several is told by what it changes: tabs where it switches the content below, radio buttons where it sets a value, a button group where each button toggles on its own. 请改用 Tabs, Radio button, Button group。

基于没有此组件的系统构建的产品仍可使用它:在你自己的层中引入它(你自己的设计系统),并导入它自己的模块。这样它会用你的系统自己的令牌绘制。

"components": {
  "include": ["segmented-button"]
}
import '@tanrei/web/components/segmented-button';

单选、带图标、纯图标

以 m3-like 显示

用法

分段按钮容纳分段的方式,与各平台容纳重复子项的方式相同:在 Web 上是子元素,在 SwiftUI 上是视图构建器中的视图,在 Compose 上是内容 lambda 中的调用,在 Flutter 上是 segments 列表,在 React Native 上是 items 数组。选择属于整组,以值表示。selection 说明它是一个值还是多个值。

<tanrei-segmented-button label="范围" value="week">
  <tanrei-segment value="day" label="日"></tanrei-segment>
  <tanrei-segment value="week" label="周"></tanrei-segment>
  <tanrei-segment value="month" label="月"></tanrei-segment>
</tanrei-segmented-button>

<!-- 同时选中多个。values 是一个属性(property,数组);对应的 attribute 接受 JSON 数组或以空格分隔的值。 -->
<tanrei-segmented-button id="days" selection="multiple" size="small" label="天" values="mon tue">
  <tanrei-segment value="mon" label="周一"></tanrei-segment>
  <tanrei-segment value="tue" label="周二"></tanrei-segment>
  <tanrei-segment value="wed" label="周三" disabled></tanrei-segment>
</tanrei-segmented-button>

<script type="module">
  import '@tanrei/web/components/segmented-button';
  // 元素先更新自己的 value 或 values,然后触发 change。
  document.querySelector('[label="范围"]').addEventListener('change', (e) => showRange(e.detail.value));
  document.querySelector('#days').addEventListener('change', (e) => setDays(e.detail.values));
</script>
// 默认系统不包含分段按钮:它在 m3-like、m3-expressive-like 和 cupertino 的入口中,也在 /all 中。
import { Segment, SegmentedButton } from '@tanrei/react/m3-like';

// 单选时 detail 是 { value },多选时是 { values }。
<SegmentedButton label="范围" value={range} onChange={(e) => 'value' in e.detail && setRange(e.detail.value)}>
  <Segment value="day" label="日" />
  <Segment value="week" label="周" />
  <Segment value="month" label="月" />
</SegmentedButton>

<SegmentedButton selection="multiple" label="天" values={days} onChange={(e) => 'values' in e.detail && setDays(e.detail.values)}>
  <Segment value="mon" label="周一" />
  <Segment value="tue" label="周二" />
  <Segment value="wed" label="周三" disabled />
</SegmentedButton>
import Tanrei

// 绑定的类型决定选择方式:可选值表示选一个,Set 表示选多个。
// 把 .tag 和 .labelStyle 直接加在 TanreiSegment 上,先于其他任何修饰符。
TanreiSegmentedButton("范围", selection: $range) {
    TanreiSegment("日").tag("day")
    TanreiSegment("周").tag("week")
    TanreiSegment("月").tag("month")
}

// .labelStyle(.iconOnly) 就是 labelHidden:标题仍然为分段命名。
TanreiSegmentedButton("文本对齐", selection: $alignment) {
    TanreiSegment("左对齐", icon: "format_align_left").tag("start").labelStyle(.iconOnly)
    TanreiSegment("居中", icon: "format_align_center").tag("center").labelStyle(.iconOnly)
    TanreiSegment("右对齐", icon: "format_align_right").tag("end").labelStyle(.iconOnly)
}
import io.github.chnotchy.tanrei.components.TanreiSegment
import io.github.chnotchy.tanrei.components.TanreiSegmentedButton
import io.github.chnotchy.tanrei.components.TanreiSegmentedButtonSelectionMode
import io.github.chnotchy.tanrei.primitives.TanreiText
import io.github.chnotchy.tanrei.tokens.TanreiSegmentedButtonSize

// 两种选择方式共用一个回调。单选的一组传入只含一个元素的 Set。
TanreiSegmentedButton(
    onSelectionChange = { range = it.single() },
    contentDescription = "范围",
    selectedValue = range,
) {
    TanreiSegment(value = "day", label = "日")
    TanreiSegment(value = "week", label = "周")
    TanreiSegment(value = "month", label = "月")
}

TanreiSegmentedButton(
    onSelectionChange = { days = it },
    contentDescription = "天",
    selectionMode = TanreiSegmentedButtonSelectionMode.Multiple,
    selectedValues = days,
    size = TanreiSegmentedButtonSize.Small,
) {
    TanreiSegment(value = "mon", label = "周一")
    TanreiSegment(value = "tue", label = "周二")
    // Compose 使用含义相反的标志。
    TanreiSegment(value = "wed", label = "周三", enabled = false)
}

// labelHidden 只画图标。标签仍然为分段命名。
TanreiSegment(value = "start", label = "左对齐", labelHidden = true, icon = { TanreiText("format_align_left") })
import 'package:tanrei/tanrei.dart';

// 形式与 SegmentedButton 相同:用一个 Set 保存选择,单选时最多只有一个元素。
TanreiSegmentedButton<String>(
  semanticLabel: '范围',
  selected: <String>{range},
  onSelectionChanged: (Set<String> value) => setState(() => range = value.single),
  segments: const <TanreiSegment<String>>[
    TanreiSegment<String>(value: 'day', label: '日'),
    TanreiSegment<String>(value: 'week', label: '周'),
    TanreiSegment<String>(value: 'month', label: '月'),
  ],
);

TanreiSegmentedButton<String>(
  semanticLabel: '天',
  multiSelectionEnabled: true,
  size: TanreiSegmentedButtonSize.small,
  selected: days,
  onSelectionChanged: (Set<String> value) => setState(() => days = value),
  segments: const <TanreiSegment<String>>[
    TanreiSegment<String>(value: 'mon', label: '周一'),
    TanreiSegment<String>(value: 'tue', label: '周二'),
    TanreiSegment<String>(value: 'wed', label: '周三', enabled: false),
  ],
);

// labelHidden 只画图标。标签仍然为分段命名。
const TanreiSegment<String>(value: 'start', label: '左对齐', icon: Icon(AppSymbols.formatAlignLeft), labelHidden: true);
import { SegmentedButton } from '@tanrei/react-native';

// 受控:onSelectionChange 收到一个值(单选)或多个值(多选)。
<SegmentedButton
  accessibilityLabel="范围"
  value={range}
  onSelectionChange={setRange}
  items={[
    { value: 'day', label: '日' },
    { value: 'week', label: '周' },
    { value: 'month', label: '月' },
  ]}
/>

<SegmentedButton
  selectionMode="multiple"
  accessibilityLabel="天"
  size="small"
  values={days}
  onSelectionChange={setDays}
  items={[
    { value: 'mon', label: '周一' },
    { value: 'tue', label: '周二' },
    { value: 'wed', label: '周三', disabled: true },
  ]}
/>

// 纯图标的分段:{ value: 'start', label: '左对齐', icon: 'format_align_left', labelHidden: true }

规格

尺寸与禁用的分段

以 m3-like 显示

构成

部件 说明
1 set The row that holds the segments: its fill, its outline and its outer corners. It gives the segments equal widths, draws the dividers between them and moves keyboard focus among them.
2 container One segment: the area that carries hover, focus and press, its selected fill, and its hit area.
3 check · 可选 The check a selected segment shows before its label, in place of its icon when it has one: the glyph check of the system's icon set, drawn with the icon set's family, fill, weight and optical size. The segmented button draws no mark of its own. Not drawn in an icon-only segment, nor where the system's check size is 0.
4 icon · 可选 A glyph before the label, or alone in an icon-only segment.
5 label The segment's text and its accessible name. It may be hidden for an icon-only segment (labelHidden) and still name it.
6 divider The line between two neighbouring segments.
7 outline The border around the whole set, drawn inside its edge.
8 focus-ring Gap-then-ring indicator shown for keyboard focus, drawn inside the focused segment.

属性

属性 类型 默认值 说明
selection single | multiple "single" single lets people choose one option at a time, as a radio group does; multiple lets them switch each option on and off.
value string null The value of the selected segment while selection is single; null selects none. The segmented button sets it when a segment is activated and emits change; the product may also set it.
values strings The values of the selected segments while selection is multiple: a list of strings (the platforms take a set). An empty list selects none.
size small | medium | large "medium" Visual height (32 / 36 / 40), matching Button's heights so that the set can share a row with buttons. The hit area always meets the minimum target. A design system may add values.
label string "" The accessible name of the set: the question its options answer ("Range", "Text alignment"). Required.

事件

事件 说明
change The user changed the selection; carries value (single) or values (multiple) after the change, which the set now holds.

条目

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

segment

One option of the set: a label, an optional icon, and the value that selects it.

属性 类型 默认值 说明
value string "" Identifies the segment: the set's value or values name it by this, and change carries it. Unique within the set; required.
label string "" The segment's text and its accessible name. Required, even when labelHidden is true.
icon string "" The name of an icon (a glyph of the system's icon set). Give every segment of a set an icon, or none.
labelHidden boolean false Draws the icon alone and keeps label as the accessible name (an icon-only segment).
disabled boolean false Prevents choosing or clearing the segment; the arrow keys skip it.
规格 Web SwiftUI Jetpack Compose Flutter React Native
条目 tanrei-segment TanreiSegment TanreiSegment TanreiSegment Segment
value · 属性 value tag value value value
label · 属性 label title label label label
icon · 属性 icon icon icon icon icon
labelHidden · 属性 labelHidden labelStyle labelHidden labelHidden labelHidden
disabled · 属性 disabled disabled enabled enabled disabled
备注 A child element: <tanrei-segment value="week" label="Week" icon="…">, with label-hidden and disabled attributes, as <option> in a <select>. TanreiSegment(_ title:, icon:).tag(value) in the view builder, as a Picker's tagged options; .labelStyle(.iconOnly) is labelHidden; disabled follows .disabled(_:). TanreiSegment(value, label, icon, labelHidden, enabled) called in the TanreiSegmentedButton { } content lambda. enabled is the inverse flag. TanreiSegment<T>(value:, label:, icon:, labelHidden:, enabled:) in the segments: list. enabled is the inverse flag. The element type of the set's items array: { value, label, icon?, labelHidden?, disabled? }, where icon is the glyph's name.

状态

行为

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

ID 需求
SEG-1 WHEN the user activates an enabled segment by pointer, touch, Enter or Space WHILE selection is single and value is not that segment's value THE segmented button SHALL set value to the segment's value, so that the segment that was selected becomes unselected, and emit change exactly once with it; WHEN the segment is already selected THE segmented button SHALL change nothing and emit nothing, because a single choice is never cleared by its own segment.
SEG-2 WHEN the user activates an enabled segment by pointer, touch, Enter or Space WHILE selection is multiple THE segmented button SHALL add the segment's value to values, or remove it when values holds it, and emit change exactly once with the new values; values may become empty.
SEG-3 WHILE a segment's disabled is true THE segmented button SHALL NOT select or clear it or emit change for it, SHALL skip it with the arrow keys, SHALL NOT make it the Tab stop and SHALL expose the disabled state to assistive technology.
SEG-4 WHILE a segment is hovered, keyboard-focused or pressed THE segmented button SHALL render its container, label, icon, check, elevation and opacity from the component tokens of its condition (selected or unselected) in the highest-precedence active state, with precedence pressed over focused over hovered; focus gained by pointer or touch SHALL NOT select the focused state. A part the interaction states recolor changes over the component's state motion; only a change of condition uses the selection motion (SEG-16).
SEG-5 WHEN a segment receives keyboard focus THE segmented button SHALL show the focus ring inside the segment's container, where the set's outline and the neighbouring segments cannot cover or clip it, without animation; focus gained by pointer or touch SHALL NOT show it.
SEG-6 WHERE the set is shorter than the minimum target THE segmented button SHALL extend each segment's hit area above and below the set to sys.target.touch for touch input and sys.target.pointer for fine pointers without changing its visual size, and SHALL make each segment at least as wide as that target, because neighbouring segments cannot extend a hit area over each other; a platform that cannot tell the input type SHALL use sys.target.touch.
SEG-7 WHILE a segment's disabled is true THE segmented button SHALL render its container, label, icon, check, elevation and opacity from the disabled component tokens of its condition; the set's outline and the dividers SHALL keep their colors, so that the set keeps a visible boundary.
SEG-8 WHEN size names a value that the active design system does not have THE segmented button SHALL render with the default value's tokens, and in debug builds report the unknown value.
SEG-9 WHEN the segment is pressed by pointer, touch, Enter or Space THE segment 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.
SEG-10 WHILE pressed THE segment 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, check, 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.segmented-button.motion.feedback-duration with comp.segmented-button.motion.state-easing, fades over the state duration of its size (comp.segmented-button.motion.state-duration.small, .medium or .large) with comp.segmented-button.motion.state-easing once the press has ended and press-hold has passed, and appears whole where motion is reduced.
SEG-11 THE segment 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.
SEG-12 THE segmented button SHALL render a segment from the selected condition (comp.segmented-button.selected.container.color and the other selected parts) WHILE selection is single and value equals the segment's value, or selection is multiple and values holds it, and every other segment from the unselected condition (comp.segmented-button.unselected.container.color and so on); WHEN value or values names no segment THE segmented button SHALL select nothing for that name, and WHEN a segment's value is empty or repeats another segment's THE segmented button SHALL still render it and treat the first segment with a value as the one that value selects; in debug builds it SHALL report each of these.
SEG-13 THE segmented button SHALL lay its segments out in one row, in order, in a set at least the container height of its size tall (comp.segmented-button.container.height.medium for medium, and so on), filled with comp.segmented-button.set.color, with outer corners of comp.segmented-button.set.shape, an outline of comp.segmented-button.outline.width in comp.segmented-button.outline.color inside its edge, and its segments inset from that edge by comp.segmented-button.set.padding; the outer corners of the first and last segment SHALL be the set's corners less set.padding, so that the two curves stay concentric, and every other corner of a segment SHALL be comp.segmented-button.container.shape; each segment SHALL clip its fill, state layer and ripple to its own shape, and the set SHALL NOT clip a segment's elevation.
SEG-14 THE segmented button SHALL give every segment the same width: the widest segment's content plus the padding of its size on each side (comp.segmented-button.container.padding-inline.medium for medium, and so on), a segment's content being its leading glyph (its icon, or the check where one would be drawn), the gap of its size (comp.segmented-button.container.gap.medium, and so on) and its label, measured as if the segment were selected, so that selecting a segment never changes a width; WHERE the layout gives the set more width THE segmented button SHALL divide it equally among the segments, and WHERE less, it SHALL divide what it has equally and let labels wrap onto more lines rather than truncate, growing every segment to the tallest, with at least the padding-block of its size (comp.segmented-button.container.padding-block.medium, and so on) above and below.
SEG-15 THE segmented button SHALL centre a segment's content in it, the leading glyph first; SHALL set the label in the label style of its size (comp.segmented-button.label.typescale.medium, and so on) and the label color of its condition and state; and SHALL draw the icon in the icon color of its condition and state at the icon size of its size (comp.segmented-button.icon.size.medium, and so on) through the icon helper, with the family and fill of the segment's condition (selected for a selected segment, otherwise unselected), sys.icon.weight and an optical size clamped between sys.icon.optical-size.min and sys.icon.optical-size.max.
SEG-16 WHILE a segment is selected and its label is drawn THE segmented button SHALL draw the glyph check of the system's icon set in the leading position, in place of the segment's icon when it has one, as the filter chip draws its check (CHP-13, CHP-14), at the check size of its size (comp.segmented-button.check.size.medium, and so on) in the icon color of the selected condition, with sys.icon.family.selected, sys.icon.fill.selected, the weight comp.segmented-button.check.weight (not sys.icon.weight: the check is a control's own mark drawn on the selected fill, as the checkbox's is, CHK-12) and an optical size clamped between sys.icon.optical-size.min and sys.icon.optical-size.max, and SHALL NOT mirror it when the layout direction is right-to-left; the weight SHALL change the stroke of the glyph only, the check's box staying its check size; THE segmented button SHALL draw no mark of its own (no path, shape or character of its own for the check). An icon-only segment SHALL keep its icon when selected and draw no check; WHILE the check size of the segment's size is 0 THE segmented button SHALL draw no check, reserve no room for it, and keep the icon. WHEN a segment becomes selected or unselected THE segmented button SHALL fade the check in or out (a glyph is not drawn along a path), cross-fading it with the icon where the segment has one, and change the segment's colors and elevation over comp.segmented-button.motion.selection-duration with comp.segmented-button.motion.selection-easing, re-centring the content as the check comes and goes, and SHALL show the new condition at once where motion is reduced.
SEG-17 THE segmented button SHALL draw a line of comp.segmented-button.divider.width in comp.segmented-button.divider.color between each two neighbouring segments, taking that width between them and running from the set's top padding to its bottom padding; the line SHALL keep its color and full opacity whatever the state or condition of either segment, and SHALL be hidden from assistive technology.
SEG-18 WHILE selection is single THE segmented button SHALL be one Tab stop — the selected segment, or the first enabled segment while none is selected; WHILE a segment has keyboard focus Right Arrow and Down Arrow SHALL move focus to the next enabled segment and Left Arrow and Up Arrow to the previous one, wrapping at the ends, and Home and End to the first and last enabled segment, and each move SHALL also select the segment it reaches and emit change, as a radio group does; in a right-to-left layout Left Arrow SHALL move to the next segment and Right Arrow to the previous.
SEG-19 WHILE selection is multiple THE segmented button SHALL be one Tab stop — the segment that had focus last, or else the first selected segment, or else the first enabled segment; WHILE a segment has keyboard focus Right Arrow and Down Arrow SHALL move focus to the next enabled segment and Left Arrow and Up Arrow to the previous one, wrapping at the ends, and Home and End to the first and last enabled segment, without changing the selection, and Space or Enter SHALL switch the focused segment (SEG-2); in a right-to-left layout Left Arrow SHALL move to the next segment and Right Arrow to the previous.
SEG-20 WHILE selection is single THE segmented button SHALL expose the set as a radio group named by label and each segment as a radio with its checked state; WHILE selection is multiple THE segmented button SHALL expose the set as a group named by label and each segment as a toggle button with its pressed state, as a toggle Button is; in both, each segment SHALL be named by its own label whether or not it is drawn, over a name the product gives the segment on the element (aria-label or aria-labelledby) and expose its position and the number of segments, and the icons, the check, the dividers and the outline SHALL be hidden.
SEG-21 WHEN the layout direction is right-to-left THE segmented button SHALL order the segments from the right, mirror each segment's content (the glyph to the right of the label) and apply its paddings by logical start and end, and SHALL grow a ripple from the point of contact as in left-to-right.
SEG-22 WHILE a segment's label is empty and the product gives the segment a name of its own on the element — aria-label, or aria-labelledby naming an element — THE segmented button SHALL expose that name as the segment's accessible name; WHILE a segment's label is empty and the product gives it none THE segmented button SHALL still render the segment, and in debug builds report that it has no accessible name; WHILE a segment's labelHidden is true and its icon is empty THE segmented button SHALL draw the label anyway and in debug builds report the segment; WHILE the set's label is empty, or the set holds fewer than two or more than five segments, THE segmented button SHALL still render and in debug builds report it.
SEG-23 WHEN the colors or the elevation of a segment change between interaction states THE segmented button SHALL animate them over the state duration of its size (comp.segmented-button.motion.state-duration.small, .medium or .large) with comp.segmented-button.motion.state-easing, and SHALL change them at once where motion is reduced.
SEG-24 WHILE a segment is selected THE segmented button SHALL cast the elevation of its condition and state (comp.segmented-button.selected.container.elevation at rest, and so on) outside the segment and above its neighbours; THE segmented button SHALL NOT position itself or draw anything outside the set but that elevation.
SEG-25 IF the icon set cannot draw the check yet (its font is still loading or failed to load) THE segmented button SHALL draw nothing in the check's place, never the glyph's name as text, SHALL keep the room SEG-14 measures for it so that no width changes, SHALL still expose the segment's selected state, and SHALL show the check once the icon set can draw it, without the selection motion of SEG-16.
SEG-26 THE segmented 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 segmented button is shown THE segmented button SHALL draw itself for the new direction at once, without a motion of its own, keeping its selection and its focus.
SEG-27 WHERE the layout direction is right-to-left THE segmented button SHALL draw a segment's icon 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 SEG-21 gives. THE segmented 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. The check of a selected segment is never mirrored (SEG-16).
SEG-28 THE segmented button SHALL lay out a segment'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 segmented button SHALL still place each label and align its lines by the layout direction, not by the text's own.
SEG-29 WHERE the platform has ARIA attributes (the web), WHEN the product sets an aria- attribute on a segment's host, by the attribute or by the ARIA property that reflects it, THE segmented 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 the segment's 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 segment is invalid — except those a segment owns (aria-checked, aria-pressed, 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 segmented button SHALL take it off that element. WHEN the product sets an aria- attribute a segment owns THE segmented button SHALL report it in debug builds. The set's host carries its radio group or group role itself (SEG-20), so what the product writes on it is not moved, but for a name: WHILE the set's label is not empty THE segmented button SHALL hold the product's aria-label and aria-labelledby on that host as their data-aria- twins, so that label is the set's whole name although the platform gives aria-labelledby the precedence, and WHEN label is emptied SHALL give them back as they were written; WHILE it is empty a name the product gives so is the set's name and SHALL NOT be reported as having none in debug builds (SEG-22); WHEN the product removes one, or its data-aria- twin, or sets the ARIA property that reflects it to null, THE segmented button SHALL take it away.

各平台名称对照

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

规格 Web SwiftUI Jetpack Compose Flutter React Native
组件 tanrei-segmented-button TanreiSegmentedButton TanreiSegmentedButton TanreiSegmentedButton SegmentedButton
selection · 属性 selection selection selectionMode multiSelectionEnabled selectionMode
value · 属性 value selection selectedValue selected value
values · 属性 values selection selectedValues selected values
size · 属性 size size size size size
label · 属性 label label contentDescription semanticLabel accessibilityLabel
change · 事件 change selection onSelectionChange onSelectionChanged onSelectionChange
备注 tanrei-segmented-button (selection, value, values, size, label) holds tanrei-segment children. value is an attribute and a property; values is a property (an array); the values attribute is also read, as JSON or as space-separated words, because the fixture harness writes attributes. It sets its own value or values before firing change, as a native control does. It puts role radiogroup (single) or group (multiple) and aria-label on its own element, and on each segment's inner <button> role radio with aria-checked, aria-posinset and aria-setsize (single), or aria-pressed (multiple), with a roving tabindex. A toggle button may not carry aria-posinset, so in a multiple set the row is a role="list" and each segment a role="listitem" that carries the position; ElementInternals is not used for the roles, because axe cannot see it. It is not form-associated. The check is the icon helper's check glyph in the check part (SEG-16). TanreiSegmentedButton(_ label:, selection: Binding<V?>, size:) { TanreiSegment … } for single selection and TanreiSegmentedButton(_ label:, selection: Binding<Set<V>>, size:) { … } for multiple: the binding's type is selection, and writing it is change. SwiftUI's Picker with .pickerStyle(.segmented) is not used: its segments cannot be tokenised. A single-choice segment is a button with the .isSelected trait and its position as its accessibility value; a multiple-choice segment adds the .isToggle trait (iOS 17). Lagging behind SEG-16 and SEG-25: without an installed icon font it strokes the check as a vector of its own (TanreiChipGlyph, on the checkbox's mark geometry) instead of drawing nothing. Lagging behind SEG-16's weight (the check drawn at comp.segmented-button.check.weight): its check is drawn at sys.icon.weight. One composable for both selections (Material's SingleChoiceSegmentedButtonRow and MultiChoiceSegmentedButtonRow in one): TanreiSegmentedButton(selectionMode, selectedValue, selectedValues, onSelectionChange, size, contentDescription) { TanreiSegment(…) }. State hoisting: the caller keeps selectedValue or selectedValues and moves it from onSelectionChange. The row is a selectableGroup(); a single-choice segment is selectable(role = Role.RadioButton) and a multiple-choice one toggleable(role = Role.Button), as the filter chip is. Compose has no radio group role. onSelectionChange is (Set<String>) -> Unit for both selections: a single choice passes a set of one (as the list does). Lagging behind SEG-16 and SEG-25: without an installed icon font it strokes the check as a vector of its own (TanreiDrawnGlyph, tanreiCheckPath) instead of drawing nothing. Lagging behind SEG-16's weight (the check drawn at comp.segmented-button.check.weight): its check is drawn at sys.icon.weight. Shaped like Flutter's SegmentedButton<T> but built with the widgets library, without Material: TanreiSegmentedButton<T>(segments:, selected: Set<T>, onSelectionChanged:, multiSelectionEnabled:, size:, semanticLabel:). selected holds zero or one value while multiSelectionEnabled is false; the caller moves it from onSelectionChanged. A single-choice segment is Semantics(inMutuallyExclusiveGroup: true, checked:) and a multiple-choice one Semantics(button: true, toggled:), inside a Semantics(container: true, label:) for the set. A single-choice set is exposed with SemanticsRole.radioGroup, named by semanticLabel (SEG-20). Lagging behind SEG-25: it writes the check as the Material Symbols code point U+E5CA (tanreiCheckGlyph), and without the icon font it draws whatever the fallback face has for that code point instead of nothing. Lagging behind SEG-16's weight (the check drawn at comp.segmented-button.check.weight): its check is drawn at sys.icon.weight. <SegmentedButton items={Segment[]} selectionMode value | values onSelectionChange size accessibilityLabel>; controlled, the caller moves value or values from onSelectionChange. The set has the radiogroup role (single) or the group role (multiple); each segment the radio role with accessibilityState.checked, or the button role with accessibilityState.selected, as a toggle Button is. The arrow keys work where a hardware keyboard is attached. Lagging behind SEG-25: it writes the ligature check into the glyph box, and without the icon font it prints the word instead of drawing nothing. Lagging behind SEG-16's weight (the check drawn at comp.segmented-button.check.weight): its check is drawn at sys.icon.weight.

令牌

令牌 引用 解析值(浅色)
--tanrei-comp-segmented-button-check-size-large sys.icon.extra-small 16px
--tanrei-comp-segmented-button-check-size-medium sys.icon.extra-small 16px
--tanrei-comp-segmented-button-check-size-small sys.icon.extra-small 16px
--tanrei-comp-segmented-button-check-weight sys.icon.mark.weight 500
--tanrei-comp-segmented-button-container-gap-large sys.space.space100 8px
--tanrei-comp-segmented-button-container-gap-medium sys.space.space100 8px
--tanrei-comp-segmented-button-container-gap-small sys.space.space100 8px
--tanrei-comp-segmented-button-container-height-large — 40px
--tanrei-comp-segmented-button-container-height-medium — 36px
--tanrei-comp-segmented-button-container-height-small — 32px
--tanrei-comp-segmented-button-container-padding-block-large — 6px
--tanrei-comp-segmented-button-container-padding-block-medium — 6px
--tanrei-comp-segmented-button-container-padding-block-small — 6px
--tanrei-comp-segmented-button-container-padding-inline-large sys.space.space200 16px
--tanrei-comp-segmented-button-container-padding-inline-medium sys.space.space175 14px
--tanrei-comp-segmented-button-container-padding-inline-small sys.space.space150 12px
--tanrei-comp-segmented-button-container-shape sys.shape.corner.none 0px
--tanrei-comp-segmented-button-disabled-container-shape — 0px
--tanrei-comp-segmented-button-disabled-opacity — 1
--tanrei-comp-segmented-button-divider-color sys.color.outline #848484
--tanrei-comp-segmented-button-divider-width sys.border.hairline 1px
--tanrei-comp-segmented-button-focused-container-shape — 0px
--tanrei-comp-segmented-button-focused-opacity — 1
--tanrei-comp-segmented-button-hovered-container-shape — 0px
--tanrei-comp-segmented-button-hovered-opacity — 1
--tanrei-comp-segmented-button-icon-size-large sys.icon.extra-small 16px
--tanrei-comp-segmented-button-icon-size-medium sys.icon.extra-small 16px
--tanrei-comp-segmented-button-icon-size-small sys.icon.extra-small 16px
--tanrei-comp-segmented-button-motion-feedback-duration sys.motion.role.feedback 450ms
--tanrei-comp-segmented-button-motion-selection-duration sys.motion.role.selection.mark 250ms
--tanrei-comp-segmented-button-motion-selection-easing sys.motion.easing.state cubic-bezier(0.2, 0, 0, 1)
--tanrei-comp-segmented-button-motion-state-duration-large sys.motion.role.state.large 250ms
--tanrei-comp-segmented-button-motion-state-duration-medium sys.motion.role.state.default 250ms
--tanrei-comp-segmented-button-motion-state-duration-small sys.motion.role.state.compact 200ms
--tanrei-comp-segmented-button-motion-state-easing sys.motion.easing.state cubic-bezier(0.2, 0, 0, 1)
--tanrei-comp-segmented-button-outline-color sys.color.outline #848484
--tanrei-comp-segmented-button-outline-width sys.border.hairline 1px
--tanrei-comp-segmented-button-pressed-container-shape — 0px
--tanrei-comp-segmented-button-pressed-opacity — 1
--tanrei-comp-segmented-button-selected-container-color sys.color.secondary-container #eeeeee
--tanrei-comp-segmented-button-selected-container-elevation sys.elevation.level0
--tanrei-comp-segmented-button-selected-disabled-container-color — #1b1b1b · 12%
--tanrei-comp-segmented-button-selected-disabled-container-elevation —
--tanrei-comp-segmented-button-selected-disabled-icon-color — #1b1b1b · 38%
--tanrei-comp-segmented-button-selected-disabled-label-color — #1b1b1b · 38%
--tanrei-comp-segmented-button-selected-disabled-state-layer-color — #1b1b1b · 38%
--tanrei-comp-segmented-button-selected-focused-container-color — #dadada
--tanrei-comp-segmented-button-selected-focused-container-elevation —
--tanrei-comp-segmented-button-selected-focused-icon-color — #262626
--tanrei-comp-segmented-button-selected-focused-label-color — #262626
--tanrei-comp-segmented-button-selected-focused-state-layer-color — #262626
--tanrei-comp-segmented-button-selected-hovered-container-color — #dedede
--tanrei-comp-segmented-button-selected-hovered-container-elevation —
--tanrei-comp-segmented-button-selected-hovered-icon-color — #262626
--tanrei-comp-segmented-button-selected-hovered-label-color — #262626
--tanrei-comp-segmented-button-selected-hovered-state-layer-color — #262626
--tanrei-comp-segmented-button-selected-icon-color sys.color.on-secondary-container #262626
--tanrei-comp-segmented-button-selected-label-color sys.color.on-secondary-container #262626
--tanrei-comp-segmented-button-selected-pressed-container-color — #dadada
--tanrei-comp-segmented-button-selected-pressed-container-elevation —
--tanrei-comp-segmented-button-selected-pressed-icon-color — #262626
--tanrei-comp-segmented-button-selected-pressed-label-color — #262626
--tanrei-comp-segmented-button-selected-pressed-state-layer-color — #262626
--tanrei-comp-segmented-button-selected-state-layer-color comp.segmented-button.selected.label.color #262626
--tanrei-comp-segmented-button-set-color — #000000 · 0%
--tanrei-comp-segmented-button-set-padding sys.space.space0 0px
--tanrei-comp-segmented-button-set-shape sys.shape.corner.small 6px
--tanrei-comp-segmented-button-unselected-container-color — #000000 · 0%
--tanrei-comp-segmented-button-unselected-container-elevation sys.elevation.level0
--tanrei-comp-segmented-button-unselected-disabled-container-color — #000000 · 0%
--tanrei-comp-segmented-button-unselected-disabled-container-elevation —
--tanrei-comp-segmented-button-unselected-disabled-icon-color — #1b1b1b · 38%
--tanrei-comp-segmented-button-unselected-disabled-label-color — #1b1b1b · 38%
--tanrei-comp-segmented-button-unselected-disabled-state-layer-color — #1b1b1b · 38%
--tanrei-comp-segmented-button-unselected-focused-container-color — #1b1b1b · 10%
--tanrei-comp-segmented-button-unselected-focused-container-elevation —
--tanrei-comp-segmented-button-unselected-focused-icon-color — #4b4b4b
--tanrei-comp-segmented-button-unselected-focused-label-color — #1b1b1b
--tanrei-comp-segmented-button-unselected-focused-state-layer-color — #1b1b1b
--tanrei-comp-segmented-button-unselected-hovered-container-color — #1b1b1b · 8%
--tanrei-comp-segmented-button-unselected-hovered-container-elevation —
--tanrei-comp-segmented-button-unselected-hovered-icon-color — #4b4b4b
--tanrei-comp-segmented-button-unselected-hovered-label-color — #1b1b1b
--tanrei-comp-segmented-button-unselected-hovered-state-layer-color — #1b1b1b
--tanrei-comp-segmented-button-unselected-icon-color sys.color.on-surface-variant #4b4b4b
--tanrei-comp-segmented-button-unselected-label-color sys.color.on-surface #1b1b1b
--tanrei-comp-segmented-button-unselected-pressed-container-color — #1b1b1b · 10%
--tanrei-comp-segmented-button-unselected-pressed-container-elevation —
--tanrei-comp-segmented-button-unselected-pressed-icon-color — #4b4b4b
--tanrei-comp-segmented-button-unselected-pressed-label-color — #1b1b1b
--tanrei-comp-segmented-button-unselected-pressed-state-layer-color — #1b1b1b
--tanrei-comp-segmented-button-unselected-state-layer-color comp.segmented-button.unselected.label.color #1b1b1b
--tanrei-comp-segmented-button-label-typescale-large sys.typescale.label 500 14px/1.4286
--tanrei-comp-segmented-button-label-typescale-medium sys.typescale.label 500 14px/1.4286
--tanrei-comp-segmented-button-label-typescale-small sys.typescale.label 500 14px/1.4286

测试样例

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

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

指南

何时使用

当一个小问题只有几个固定的答案,并且用户应当一眼看到全部答案时,使用分段按钮:图表的范围、列表的布局、段落的对齐方式、提醒在哪几天重复。选择的效果应当立即显现,因为单选时方向键在移动的同时就选中了。

适合两到五个选项。超过五个,请使用菜单或列表。当这项选择是表单的主要任务时,用单选按钮更清楚。当答案会改变填满屏幕的内容时,请使用标签页。

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

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

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

一个快速的判断方法:如果按下它会执行某件事,那是按钮组。如果它设置一个值,那是分段按钮;选项很多或相互独立时,则是纸片。如果它替换下方的内容,那是标签页。

选一个还是选多个

  • 单选(selection="single",默认)是一个单选组。选中一个分段会取消之前选中的那一个。再次按下已选中的分段不起任何作用,所以单选的选择无法通过它自己的分段来清除。这一组可以在开始时什么都不选(value 为空),用于必须表明“尚未选择”的表单。
  • 多选是一组切换按钮。每个分段各自打开和关闭,整组最后可以一个都不选。如果你的产品要求至少选一个,就不要应用会让整组变空的更改。

整组没有自己的 disabled。请逐个禁用分段。禁用的分段保留自己的位置和轮廓,方向键会跳过它。

标签、图标与对勾

  • 选中的分段在标签之前显示一个对勾;如果它有图标,对勾取代图标。因此,选择状态绝不会只靠填充来表达。纯图标的分段保留自己的图标,并把它画成填充样式。设计系统可以把对勾的尺寸设为 0:这时它不画对勾,而用填充和层级来显示选择状态。
  • 每个分段都与最宽的那个等宽,宽度按它被选中时的样子来测量,所以对勾出现时绝不会挤动相邻的分段。空间不够时,标签会换成多行,每个分段都增高到与最高的那个等高。标签绝不截断。
  • labelHidden 只画图标。标签仍然是必填的,因为它始终是分段的无障碍名称。
  • 尺寸决定整组的高度。这些尺寸并不是每一级都等于 Button 的高度(在 m3-like 预设中,small 和 medium 与 Button 的相同,large 则更矮),所以让一组分段与旁边的按钮相配时,要按高度,而不是按尺寸的名称。

推荐

  • 给整组一个标签,提出它的选项所回答的那个问题:“范围”“文本对齐”。
  • 标签保持在一到两个词,并且形式一致:日、周、月。
  • 一组里的分段要么都带图标,要么都不带。
  • 有合理的默认值时,选定一个初始值,这样控件看起来绝不会像没有作答。

避免

  • 不要把分段按钮用于操作。分段是一个选项,不触发 click。请使用按钮组。
  • 不要用它在各自填满屏幕的视图之间切换。请使用标签页。
  • 不要在一组里放五个以上的分段,也不要使用长短悬殊的标签。每个分段都取最宽那个的宽度。
  • 不要让分段的标签随状态改变。选择状态是单独朗读的。

无障碍

角色
radiogroup
标注
The set is named by label: the question its options answer. Each segment is named by its own label, drawn or not (labelHidden). Icons, the check, the dividers and the outline are decorative: a segment's state is announced from its checked or pressed state, never from the check.
最小目标
即使外观尺寸更小,点击区域也满足最小目标尺寸。 minimum 32px · pointer 44px · spacing 8px · touch 48px

键盘操作

按键 动作
Tab / Shift+Tab Moves focus into the set (to the selected segment, the last focused one, or the first enabled one) and out of it.
Right Arrow / Down Arrow Moves focus to the next enabled segment, wrapping; with single selection it also selects it (Left Arrow in right-to-left layouts).
Left Arrow / Up Arrow Moves focus to the previous enabled segment, wrapping; with single selection it also selects it (Right Arrow in right-to-left layouts).
Home / End Moves focus to the first or last enabled segment; with single selection it also selects it.
Enter / Space Selects the focused segment (single) or switches it on or off (multiple).

备注

  • Single selection is a radio group: arrow keys move and select at once, as the radio button and tabs do, because a segment shows its effect without a noticeable delay. Multiple selection is a group of toggle buttons with one Tab stop: arrow keys only move focus, and Space or Enter switches.
  • Selection is the set's: value or values holds the selected segments' values, and change carries the new ones. A segment's disabled is its own.
  • iOS has no radio role: a segment of a single-choice set is a button with the selected trait and its position ("2 of 3") in its accessibility value, as the radio button is. Compose has no radio group role; the set is a selectable group and each segment `Role.RadioButton` (single) or a toggleable `Role.Button` (multiple), as the filter chip is.
  • A segmented button chooses among options of one setting. Actions belong to a button group; switching views of one subject belongs to tabs.
  • Text size does not clip a segment: labels wrap and every segment grows to the tallest. The icon is not text and keeps its size.
  • 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/segmented-button.spec.json, tokens/sys/measure.tokens.json

组件负责的部分

  • 单选时,整组是一个以其标签命名的单选组,每个分段是一个带有选中状态的单选按钮。多选时,整组是一个以其标签命名的组,每个分段是一个带有“已按下”状态的切换按钮。
  • 每个分段都以自己的标签命名,无论标签是否画出来,并暴露自己的位置和分段的总数。图标、对勾、分隔线和轮廓都被隐藏。状态绝不是由对勾来朗读的。
  • 整组是一个 Tab 停靠点:停在选中的分段上;多选时停在最后获得焦点的分段上;否则停在第一个可用的分段上。方向键移到下一个或上一个可用的分段并循环,Home 和 End 到达两端。单选时,每次移动同时也选中。多选时,Space 和 Enter 切换获得焦点的分段。在从右到左的布局中,左方向键移到下一个分段。
  • 禁用的分段会被方向键跳过,绝不会成为 Tab 停靠点,并暴露为禁用。
  • 每个分段的点击区域向整组的上方和下方延伸到最小目标,并且每个分段的宽度至少等于一个目标,因为相邻的分段无法把点击区域伸到彼此之上。
  • 焦点指示画在获得焦点的分段内部,轮廓和相邻的分段都遮不住它。它只在键盘聚焦时显示。
  • 文本放大时,标签换行,每个分段都增高到与最高的那个等高。图标保持自己的尺寸。
  • 调试构建会报告以下情况:缺少整组或分段的标签,隐藏了标签却没有图标,value 或 values 所指的分段不存在,分段的值为空或重复,以及一组少于两个或多于五个分段。

已知的平台差距:

  • 在 Web 上,多选的一组通过包在分段外面的一个列表来给出每个分段的位置,因为切换按钮自己无法携带位置。
  • SwiftUI 没有单选组角色。整组是一个以其标签命名的容器。单选组里的分段是一个带有选中特征的按钮,以自己的位置(“2 of 3”)作为值,目前仅有英文。多选组里的分段是一个 toggle,不说明位置,因为它如果有自己的值,就会取代 toggle 的开或关。
  • SwiftUI:整组占用提供给它的宽度,与 SwiftUI 的分段选择器(segmented picker)一样。.fixedSize(horizontal: true, vertical: false) 让它保持自然宽度。分段的阴影和焦点环在每个角上都使用它最大的那个圆角。作为图标传入的 SF Symbol 在选中时不会切换为填充样式:请自己传入 .fill 的符号。方向键是作为规则来测试的,没有在模拟器上用键盘测试。
  • Compose 没有单选组角色。整组是一个可选择的组,每个分段是一个单选按钮(单选)或一个可切换的按钮(多选),与筛选纸片相同。
  • Flutter 既不暴露分段的位置,也不暴露分段的总数;多选的一组是一个以其标签命名的容器,而不是一个组,因为 Flutter 的语义中没有这两种属性。
  • React Native:等宽是在第一次布局之后测量每个标签得到的,所以如果一组分段的父容器在第一帧时比它窄,它得到的宽度可能比它最宽的标签还窄。硬件键盘上的方向键,以及 Android 上抬升的选中分段投在相邻分段上的阴影,都还没有在真机上检查过。
  • 在 cupertino 系统下,small 和 medium 的分段按钮高度是 30 和 34,而不是 28 和 32,两端分段的内侧圆角比外侧圆角更圆。这两点都在等待令牌或规格方面的决定。

需要你做的事

  • 给整组一个标签,也给每个分段一个标签,包括纯图标的分段。
  • 让选择的效果立即在控件附近显现。单选时,键盘用户靠移动来选择,效果一慢,方向键就会显得卡住。如果确实需要时间,请在结果将要出现的地方显示进度。
  • 当分段为什么不能选并不明显时,在附近给出禁用的原因。
  • 在 Web 上,分段按钮不是表单控件。要随表单提交它的值,请在 change 中把值复制到一个隐藏的 input 里。