ToggleGroup 切换按钮组
将多个切换按钮组合为单选或多选控件。
用法
同时切换多个文本格式
组件结构
加粗的是必需部件。
data-scope="toggle-group":root · item · hidden-input
示例
受控状态
由外部状态控制选中值
多选
同时选择多个格式
禁用
禁用单个选项
方向
水平或垂直排列
宽度充满
选项等分可用宽度
尺寸
提供三种尺寸
变体
设置整组外观
无分隔线
省略分隔线部件
设计指引
何时使用
- 在少量选项之间切换视图或显示方式。
- 同时启用多个格式或工具状态。
何时不用
特性
- 支持单选和多选模式。
- 支持受控和非受控状态。
- 支持水平、垂直、全宽和三种尺寸。
- 默认在相邻条目之间显示分隔线,可通过
separators=false关闭。 - 使用 roving tabindex 管理组内键盘导航。
disallowEmpty可阻止清空最后一个选中项。collection可统一提供标签和禁用状态。- 不写结构时按标准结构铺出整组(Vue 不写默认插槽、React 不给 children、Web Components 不写部件或只写
root):每一条一个按钮,末尾一份表单影子,给了name才参与提交。Web Components 里某一条的内容可以写在宿主的直接子节点上接管:slot="item:<值>"。 - 值可以是任意类型:
value、defaultValue与collection里交进来数字、对象都行,onValueChange、v-model、插槽与renderItem拿到的是原值;Vue 与 React 按value或collection推断值的类型、按multiple推断形状——写了multiple时值是数组,没写时是一个值或null,单选却绑数组、valueKey写错字段编译即报错。数字转成字符串当键,对象值给valueKey(字段名或取值函数),同一个选项每次都是新对象(接口刚取回来)也认得出来,不给按对象身份认;键也是条目的data-value与随表单提交的值。Vue 的h()推断不了泛型,那里按宽松的类型检查;Web Components 经 property 交值,写querySelector<XhToggleGroupElement<number>>("xh-toggle-group")收窄。
组合
最佳实践
- 每组使用二到五个简短选项。
- 同组条目应保持相近宽度。
- 必须保留一个选中项时启用
disallowEmpty。
反模式
- 不要用切换按钮组代替带面板关联的标签页。
- 关闭 roving focus 时,应提供其他组内导航方式。
API 参考
产物
| 层 | 值 |
|---|---|
| 自定义元素 | <xh-toggle-group> |
| Vue 组件 | XhToggleGroupHiddenInput XhToggleGroupItem XhToggleGroupRoot |
| 组合式函数 | useToggleGroup |
| 状态机 | toggleGroupMachine |
| 皮肤 | @xihan-ui/styles/toggle-group.css |
Props
| 属性 | 类型 | 必填 | 说明 |
|---|---|---|---|
collection | ToggleGroupNode<V>[] | 条目数据,显示文本与禁用的事实源。提供后条目部件只需声明 value。 未提供时回到文本与禁用都写在条目部件上的方式。 | |
valueKey | SelectionValueKey<V> | 对象值怎样取键:字段名或取值函数。同一个选项每次都是新对象(接口刚取回来)时给它; 不给时按对象身份认。值不是对象时用不着。键也是条目的 data-value 与随表单提交的值。 | |
value | ToggleGroupValue<V> | 选中值。提供即受控:内部不再自行修改,只发 onValueChange。 | |
defaultValue | ToggleGroupValue<V> | ||
multiple | boolean | 允许多项同时选中;false 时选中一项即替换其余。 | |
ariaLabel | string | 不显示出来的名字,写成 aria-label 落在与字段标签同一个部件上。 放进字段(字段接线)或渲染了 label 部件时名字以它们为准,aria-labelledby 盖过它。 | |
disabled | boolean | 整组禁用:条目全部 aria-disabled,点击与方向键都不生效。 | |
readOnly | boolean | 只读:条目仍可聚焦、方向键照常走,点击与 Space / Enter 不改选中,也不进按压面。 | |
invalid | boolean | 校验失败标注:单选组写 aria-invalid;多选是 role=group,只写 data-invalid。 | |
required | boolean | 必填:单选组写 aria-required,表单影子写原生 required;多选只写 data-required 与原生 required。 | |
disallowEmpty | boolean | 不允许清空值:单选模式下点击当前选中项不再取消它,多选模式下不可移除最后一个。 默认 false(可以点击为无选中)。 | |
variant | ActionVariant | 变体:solid / subtle / outline / ghost,决定段的底色与描边使用方式。 | |
tone | Tone | 颜色:brand / neutral / success / warning / danger / info。 | |
size | Size | 尺寸:sm / md / lg。 | |
fullWidth | boolean | 撑满行宽:整组占满可用宽度,每段等分剩余空间。 | |
separators | boolean | 是否自动在相邻条目之间插入分隔线,默认 true。 | |
name | string | 表单字段名。提供后隐藏输入才带 name 并参与提交。 | |
orientation | Orientation | 视觉排布,默认 horizontal。方向键接受的轴与它无关(四个方向键恒响应)。 | |
dir | Direction | 文字方向,默认 ltr;只改写左右方向键的语义,上下键与之无关。 | |
loop | boolean | 方向键到达末尾是否回绕,默认 true。 | |
rovingFocus | boolean | roving tabindex,默认开启:整组只占一个 Tab 位,组内依靠方向键移动。 关闭后每个条目自成一个 Tab 停靠点,方向键不再接管。 | |
onValueChange | (details: ToggleGroupValueChangeDetails<V>) => void | value 变化意图回调;受控时是唯一出口,非受控时随内部写入一并通知。 |
ToggleGroupNode
collection 的元素。
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
value | V | 是 | |
label | string | 展示文本;默认回退为值(不是字符串的值退回它的键)。 | |
disabled | boolean | 条目禁用:方向键跳过它,但它仍可聚焦、仍是导航起点。 |
事件
自定义元素将载荷放在 detail;Vue 使用同名 emit。
| 事件 | 载荷 | 说明 |
|---|---|---|
value-change | ToggleGroupValueChangeDetails | 选中值变化;detail 为 { value: 值 | 值[] | null }(形态随 multiple 决定),值是 value / collection 里交进来的原值(字符串、数字或对象) |
React 适配器 props
只列各组件自己声明的那些:继承自 ComponentPropsWithRef 的 DOM 属性不在其中,根组件上与上面 Props 表同名的也不重复列。Vue 的对应物是上面的插槽表。
| React 组件 | 属性 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
XhToggleGroupItem | value | unknown | 是 | 条目代表的值,与根上的值同类型:字符串、数字或对象。 |
XhToggleGroupItem | disabled | boolean | 默认交给 connect 查询 collection,写死 false 会覆盖数据中的禁用。 | |
XhToggleGroupRoot | renderItem | (node: ToggleGroupNodeMeta<V>) => ReactNode | 每个条目的自定义内容;未提供时使用 collection 中的 label。 | |
XhToggleGroupRoot | children | ReactNode |
状态
公开状态写入 data-state。
| 部件 | 取值 |
|---|---|
item | 'on' | 'off' |
以下名称仅用于内部状态机。
状态:idle
事件:VALUE.SET · ITEM.TOGGLE · ITEM.FOCUS · GROUP.BLUR · FORM.RESET · PRESS.START · PRESS.END
判据:canPress · canToggle
connect API
getXxxProps() 返回对应部件的宿主属性。
| 成员 | 类型 | 说明 |
|---|---|---|
value | V[] | 当前选中集合,恒为数组(单选时长度 ≤ 1)。 |
collection | readonly ToggleGroupNodeMeta<V>[] | 由 collection 推导的条目元信息,按数据顺序排列;未提供 collection 时为空数组。 |
focusedValue | V | null | 焦点在组外时为 null。 |
multiple | boolean | |
disabled | boolean | |
readOnly | boolean | |
invalid | boolean | |
required | boolean | |
orientation | Orientation | |
separators | boolean | |
isSelected | (value: V) => boolean | |
setValue | (next: ToggleGroupValue<V>) => void | 传单值 / 数组 / null 均可,内部按 multiple 归一。 |
getRootProps | () => T['element'] | |
getItemProps | (props: ToggleGroupItemProps<V>) => T['button'] | |
getHiddenInputProps | () => T['input'] | 表单出口:整组只有一份,提交的即当前选中值。 |
无障碍
键盘
规格出处:W3C APG
| 按键 | 生效条件 | 行为 |
|---|---|---|
Tab / Shift+Tab | rovingFocus 开启(默认) | 整组只占一个 Tab 位:焦点落到锚点条目,无锚点时先落容器再由它转投 |
ArrowRight / ArrowDown | focus in group, 组未禁用且 rovingFocus 开启 | 焦点移到下一个可停留条目(禁用项跳过、尽头按 loop 回绕),不改选中;dir=rtl 时改由 ArrowLeft 承担 |
ArrowLeft / ArrowUp | focus in group, 组未禁用且 rovingFocus 开启 | 焦点移到上一个可停留条目,不改选中;dir=rtl 时改由 ArrowRight 承担 |
Home | focus in group, 组未禁用且 rovingFocus 开启 | 焦点移到首个可停留条目 |
End | focus in group, 组未禁用且 rovingFocus 开启 | 焦点移到末个可停留条目 |
Enter / Space | focus on item, 条目未禁用 | 切换该条目;条目是原生 button,这两个键由平台翻成 click |
Enter / Space | held on item, 条目未禁用且组未禁用 | 按住期间该条目投影 data-pressed,与指针 :active 同一副按压面;抬起或失焦撤下,按住途中整组转入禁用也撤下。开关态与按压互相独立,切换照旧由平台把这一次按键翻成 click |
ARIA
以下属性由 connect 生成。
| 部件 | 属性 | 值 |
|---|---|---|
root | aria-invalid | undefined | 'true' | 'false' |
root | aria-label | props.ariaLabel |
root | aria-orientation | undefined | props.orientation |
root | aria-readonly | undefined | 'true' | 'false' |
root | aria-required | undefined | 'true' | 'false' |
root | role | 'group' | 'radiogroup' |
item | aria-checked | undefined | 'true' | 'false' |
item | aria-disabled | 'true' | 'false' |
item | aria-pressed | 'true' | 'false' | undefined |
item | role | undefined | 'radio' |
hidden-input | aria-hidden | 'true' |
样式参考
皮肤
@xihan-ui/styles/toggle-group.css 按 [data-scope="toggle-group"][data-part="root"] 部件选择器书写,发布产物以挂载类 .xh-scope-toggle-group 代替其中的 data-scope(特异性相同),位于 xihan.components 层。覆盖样式使用 xihan.overrides,部件选择器照常可用。
forced-colors: active 下另有一套规则:颜色交给系统,边框与状态标记改用系统色关键字。
数据属性
由 connect 生成;条件不成立时不输出无值属性。
| 部件 | 属性 | 值 |
|---|---|---|
root | data-disabled | ''(条件成立时才出现) |
root | data-full-width | ''(条件成立时才出现) |
root | data-invalid | ''(条件成立时才出现) |
root | data-orientation | props.orientation |
root | data-readonly | ''(条件成立时才出现) |
root | data-required | ''(条件成立时才出现) |
root | data-size | props.size |
root | data-tone | props.tone |
root | data-variant | props.variant |
item | data-disabled | ''(条件成立时才出现) |
item | data-invalid | ''(条件成立时才出现) |
item | data-pressed | ''(条件成立时才出现) |
item | data-readonly | ''(条件成立时才出现) |
item | data-state | 'on' | 'off' |
item | data-xh-action-control | '' |
item | data-xh-action-display | 'always' |
item | data-xh-action-profile | 'text' |
item | data-xh-action-size | props.size |
item | data-xh-action-variant | props.variant |
CSS 变量
本组件公开覆盖槽由独立皮肤的实际消费位生成;默认来源、作用部件和状态均与 CSS 同源。
| 变量 | 部件 | CSS 属性 | 状态 | 默认来源 | 说明 |
|---|---|---|---|---|---|
--xh-toggle-group-border-invalid | root | border | invalid | --xh-border-invalid | toggle-group 的 root 部件 border 覆盖槽。 |
--xh-toggle-group-item-bg | item | --xh-ink-surfacebackground-color | defaultdisabledfocus-visiblehoveris(:active, [data-pressed])loadingnot([data-disabled])not([data-loading])pressedreadonlyxh-ink-surface | --xh-_toggle-group-item-bg | toggle-group 的 item 部件 --xh-ink-surface、background-color 覆盖槽。 |
--xh-toggle-group-item-bg-active | item | background-color | disabledis(:active, [data-pressed])loadingnot([data-disabled])not([data-loading])pressed | --xh-_toggle-group-item-bg-active | toggle-group 的 item 部件 background-color 覆盖槽。 |
--xh-toggle-group-item-bg-disabled | item | --xh-ink-surfacebackground-color | disabledxh-ink-surface | --xh-bg-muted | toggle-group 的 item 部件 --xh-ink-surface、background-color 覆盖槽。 |
--xh-toggle-group-item-bg-hover | item | background-color | disabledhoverloadingnot([data-disabled])not([data-loading]) | --xh-_toggle-group-item-bg-hover | toggle-group 的 item 部件 background-color 覆盖槽。 |
--xh-toggle-group-item-bg-on | item | --xh-ink-surfacebackground-color | disabledfocus-visiblehoveris(:active, [data-pressed])loadingnot([data-disabled])not([data-loading])pressedreadonlystate=onxh-ink-surface | --xh-_toggle-group-item-bg-on | toggle-group 的 item 部件 --xh-ink-surface、background-color 覆盖槽。 |
--xh-toggle-group-item-bg-on-active | item | background-color | disabledis(:active, [data-pressed])loadingnot([data-disabled])not([data-loading])pressedstate=on | --xh-_toggle-group-item-bg-on-active | toggle-group 的 item 部件 background-color 覆盖槽。 |
--xh-toggle-group-item-bg-on-disabled | item | --xh-ink-surfacebackground-color | disabledstate=onxh-ink-surface | --xh-_toggle-group-item-bg-on | toggle-group 的 item 部件 --xh-ink-surface、background-color 覆盖槽。 |
--xh-toggle-group-item-bg-on-hover | item | background-color | disabledhoverloadingnot([data-disabled])not([data-loading])state=on | --xh-_toggle-group-item-bg-on-hover | toggle-group 的 item 部件 background-color 覆盖槽。 |
--xh-toggle-group-item-border | item | borderborder-color | defaultdisabledfocus-visiblehoveris(:active, [data-pressed])loadingnot([data-disabled])not([data-loading])pressed | --xh-_toggle-group-item-border | toggle-group 的 item 部件 border、border-color 覆盖槽。 |
--xh-toggle-group-item-border-disabled | item | border-color | disabled | --xh-border-subtle | toggle-group 的 item 部件 border-color 覆盖槽。 |
--xh-toggle-group-item-border-on | item | borderborder-color | disabledfocus-visiblehoveris(:active, [data-pressed])loadingnot([data-disabled])not([data-loading])pressedstate=on | --xh-_toggle-group-item-border-on | toggle-group 的 item 部件 border、border-color 覆盖槽。 |
--xh-toggle-group-item-border-on-disabled | item | border-color | disabledstate=on | --xh-_toggle-group-item-border-on | toggle-group 的 item 部件 border-color 覆盖槽。 |
--xh-toggle-group-item-fg | item | color | defaultdisabledfocus-visiblehoveris(:active, [data-pressed])loadingnot([data-disabled])not([data-loading])pressed | --xh-_toggle-group-item-fg | toggle-group 的 item 部件 color 覆盖槽。 |
--xh-toggle-group-item-fg-disabled | item | color | disabled | --xh-fg-disabled | toggle-group 的 item 部件 color 覆盖槽。 |
--xh-toggle-group-item-fg-on | item | color | disabledfocus-visiblehoveris(:active, [data-pressed])loadingnot([data-disabled])not([data-loading])pressedstate=on | --xh-_toggle-group-item-fg-on | toggle-group 的 item 部件 color 覆盖槽。 |
--xh-toggle-group-item-fg-on-disabled | item | color | disabledstate=on | --xh-_toggle-group-item-fg-on | toggle-group 的 item 部件 color 覆盖槽。 |
--xh-toggle-group-item-font-size | item | font-size | default | --xh-_toggle-group-font-size | toggle-group 的 item 部件 font-size 覆盖槽。 |
--xh-toggle-group-item-font-weight | item | font-weight | default | --xh-text-label-weight | toggle-group 的 item 部件 font-weight 覆盖槽。 |
--xh-toggle-group-item-gap | item | gap | default | --xh-_toggle-group-gap | toggle-group 的 item 部件 gap 覆盖槽。 |
--xh-toggle-group-item-h | item | block-size | default | --xh-_toggle-group-h | toggle-group 的 item 部件 block-size 覆盖槽。 |
--xh-toggle-group-item-px | item | padding-inline | default | --xh-_toggle-group-px | toggle-group 的 item 部件 padding-inline 覆盖槽。 |
--xh-toggle-group-item-radius | itemroot | border-end-end-radiusborder-end-start-radiusborder-radiusborder-start-end-radiusborder-start-start-radius | first-childfirst-of-typeinvalidlast-childlast-of-typeorientation=horizontalorientation=verticalvariant=outline | --xh-shape-control | toggle-group 的 item、root 部件 border-end-end-radius、border-end-start-radius、border-radius、border-start-end-radius、border-start-start-radius 覆盖槽。 |
--xh-toggle-group-item-shadow | itemroot | box-shadow | disabledfocus-visiblehoveris(:active, [data-pressed])is(:hover, [data-loading])is(:not([data-variant])loadingnot([data-disabled])not([data-loading])pressedstate=onvariantvariant=solid | --xh-_toggle-group-item-highlight-on--xh-ring-width | toggle-group 的 item、root 部件 box-shadow 覆盖槽。 |
--xh-toggle-group-outline-color | root | border | variant=outline | --xh-_tone-border-control | toggle-group 的 root 部件 border 覆盖槽。 |
--xh-toggle-group-separator-color | root | background | xh-toggle-group-separator | --xh-fg-default | toggle-group 的 root 部件 background 覆盖槽。 |
--xh-toggle-group-separator-color-disabled | root | background | disabledxh-toggle-group-separator | --xh-border-subtle | toggle-group 的 root 部件 background 覆盖槽。 |
--xh-toggle-group-separator-opacity | root | opacity | xh-toggle-group-separator | --xh-control-separator-opacity | toggle-group 的 root 部件 opacity 覆盖槽。 |
--xh-toggle-group-separator-opacity-disabled | root | opacity | disabledxh-toggle-group-separator | --xh-control-separator-disabled-opacity | toggle-group 的 root 部件 opacity 覆盖槽。 |
--xh-toggle-group-separator-radius | root | border-radius | xh-toggle-group-separator | --xh-shape-pill | toggle-group 的 root 部件 border-radius 覆盖槽。 |
--xh-toggle-group-separator-size | root | block-sizeinline-size | orientation=horizontalorientation=verticalxh-toggle-group-separator | --xh-_group-separator-size | toggle-group 的 root 部件 block-size、inline-size 覆盖槽。 |
--xh-toggle-group-separator-thickness | root | block-sizeinline-sizemargin-block-startmargin-inline-start | orientation=horizontalorientation=verticalxh-toggle-group-separator | --xh-stroke-thin | toggle-group 的 root 部件 block-size、inline-size、margin-block-start、margin-inline-start 覆盖槽。 |
动效
动效角色:按压 · 状态(见动效规范)。
opacity 走 transition 过渡。时长与缓动读动效令牌,改令牌即改全局节奏。
系统开启减弱动效时由令牌层统一收敛,皮肤不另作判断。
RTL
皮肤用逻辑属性排布(inline-start 一族),dir="rtl" 下自动镜像。
