Base UI React ToggleGroup 完整 API 参考:Props、类型、数据属性与源码实现解析
【免费下载链接】base-uiUnstyled UI components for building accessible web apps and design systems. From the creators of Radix, Floating UI, and Material UI.项目地址: https://gitcode.com/GitHub_Trending/ba/base-ui
本指南以 Base UI(本仓库packages/react/src/toggle-group包)的ToggleGroup组件为核心,系统梳理其官方 API 参考文档(types.md/react/components/toggle-group/types.md))中的全部 Props、Data Attributes、状态与事件类型,并结合源码实现、官方示例与测试用例进行纵深解析。读完本文,你将掌握ToggleGroup的受控/非受控用法、单选与多选切换、键盘焦点循环、与Toolbar的集成方式,以及render/className/style三种渲染定制手段,可直接用于构建文本格式工具栏、分段控件(Segmented Control)等无障碍交互组件。
一、组件定位:为一系列 toggle 按钮提供共享状态
ToggleGroup是一个无样式(unstyled)的 React 组件,官方定位是"Provides a shared state to a series of toggle buttons"——即把一组Toggle按钮(来自@base-ui/react/toggle)的按下状态收敛到一个共享的组状态中,类似"按钮组"或"分段控件"。与单选组(Radio Group)不同,它默认允许全部取消选中,且可以通过multiple属性切换为多选模式,因此非常适合文本格式化工具栏(粗体/斜体/下划线)、文本对齐方式选择等场景。
该定位可以直接在源码中得到印证:ToggleGroup.tsx 的 JSDoc 注释与文档页 page.mdx/react/components/toggle-group/page.mdx#L3) 的 Subtitle 完全一致。
二、快速上手:导入与 Anatomy
组件从@base-ui/react/toggle-group单独导出,官方 Anatomy 示例(见 page.mdx/react/components/toggle-group/page.mdx#L13-L21))展示其作为"单部件"使用的方式:
import { ToggleGroup } from '@base-ui/react/toggle-group'; <ToggleGroup />;在实际使用中,ToggleGroup需要与Toggle组合。Toggle通过value属性向组内注册自己的值,而ToggleGroup用一个string[]数组(即"所有处于按下状态的 Toggle 的 value 集合")统一管理组内按下状态。完整的组合示例为:
import { ToggleGroup } from '@base-ui/react/toggle-group'; import { Toggle } from '@base-ui/react/toggle'; <ToggleGroup aria-label="Text alignment" defaultValue={['left']}> <Toggle value="left">左对齐</Toggle> <Toggle value="center">居中</Toggle> <Toggle value="right">右对齐</Toggle> </ToggleGroup>;需要注意的是,ToggleGroup渲染的根元素默认是div,并带有role="group"(见 ToggleGroup.tsx),因此建议配合aria-label提供无障碍名称。测试用例 ToggleGroup.test.tsx 也验证了queryByRole('group')的语义。
三、ToggleGroup Props 完整参考
下表完整继承自官方 API 参考文档 types.md/react/components/toggle-group/types.md#L11-L24):
| Prop | Type | Default | Description |
|---|---|---|---|
| defaultValue | string[] | - | The pressed state of the toggle group represented by an array of the values of all pressed toggle buttons. This is the uncontrolled counterpart ofvalue. |
| value | string[] | - | The pressed state of the toggle group represented by an array of the values of all pressed toggle buttons. This is the controlled counterpart ofdefaultValue. |
| onValueChange | ((groupValue: string[], eventDetails: ToggleGroup.ChangeEventDetails) => void) | - | Callback fired when the pressed states of the toggle group changes. |
| loopFocus | boolean | true | Whether to loop keyboard focus back to the first item when the end of the list is reached while using the arrow keys. |
| multiple | boolean | false | Whenfalseonly one item in the group can be pressed. If any item in the group becomes pressed, the others will become unpressed. Whentruemultiple items can be pressed. |
| disabled | boolean | false | Whether the toggle group should ignore user interaction. |
| orientation | Orientation | 'horizontal' | - |
| className | string \| ((state: ToggleGroup.State) => string \| undefined) | - | CSS class applied to the element, or a function that returns a class based on the component's state. |
| style | React.CSSProperties \| ((state: ToggleGroup.State) => React.CSSProperties \| undefined) | - | Style applied to the element, or a function that returns a style object based on the component's state. |
| render | ReactElement \| ((props: HTMLProps, state: ToggleGroup.State) => ReactElement) | - | Allows you to replace the component's HTML element with a different tag, or compose it with another component. Accepts aReactElementor a function that returns the element to render. |
3.1 受控与非受控:value / defaultValue
- 非受控:传入
defaultValue时,组件内部自行维护按下状态,用户点击后状态自动更新。官方文档将其描述为value的非受控对应物。 - 受控:传入
value时,按下状态完全由外部组件持有,必须配合onValueChange回写,否则点击后 UI 不会变化。
源码层面,这一对状态由useControlledHook 统一管理(见 ToggleGroup.tsx):
const [groupValue, setValueState] = useControlled({ controlled: valueProp, default: defaultValue, name: 'ToggleGroup', state: 'value', });当defaultValue未传时,源码会回退到EMPTY_ARRAY(ToggleGroup.tsx),并借助isValueInitialized标记区分"未传值"与"显式传空数组"两种情形,用于后续对Toggle缺失value时的数据一致性告警。
3.2 multiple:单选与多选切换
multiple是ToggleGroup最核心的行为开关:
false(默认):组内同时只能有一个按钮处于按下状态;某个按钮按下时,其余按钮自动取消按下。点击已按下的按钮会将其取消(即允许全部不选,这是与 Radio Group 的关键差异)。true:允许多个按钮同时按下,彼此独立,互不影响。
其内部实现逻辑在 ToggleGroup.tsx 的setGroupValue回调中:多选模式下对当前groupValue数组做push(按下)或splice(取消)操作;单选模式下则直接替换为[newValue]或[]。
官方在 page.mdx/react/components/toggle-group/page.mdx#L25-L31) 中给出的 Multiple 示例为:
<ToggleGroup multiple defaultValue={['bold', 'italic']} aria-label="Text formatting options"> <Toggle value="bold" aria-label="Bold" /> <Toggle value="italic" aria-label="Italic" /> <Toggle value="underline" aria-label="Underline" /> </ToggleGroup>3.3 loopFocus:键盘焦点循环
loopFocus默认true,控制使用方向键在组内移动焦点时,到达列表末尾是否回绕到第一个元素。该开关直接透传给内部基于 Composite 模式的CompositeRoot(见 ToggleGroup.tsx):
<CompositeRoot render={render} className={className} style={style} state={state} refs={[forwardedRef]} props={[defaultProps, elementProps]} loopFocus={loopFocus} enableHomeAndEndKeys orientation={orientation} />从源码结构看,ToggleGroup复用了packages/react/src/internals/composite/root/CompositeRoot来提供方向键导航能力,并且固定启用了enableHomeAndEndKeys(Home/End 键跳转首尾)。同时方向键的移动方向由orientation决定。
3.4 disabled:整组禁用
disabled默认false,为true时整个ToggleGroup忽略用户交互。值得注意的是,源码中的禁用状态是合并结果(ToggleGroup.tsx):
const disabled = (toolbarContext?.disabled ?? false) || (toolbarGroupContext?.disabled ?? false) || disabledProp;也就是说,当ToggleGroup被放置于被禁用的Toolbar或ToolbarGroup中时,即使自身未传disabled也会整体禁用。这一状态还会通过 Context 向下传给所有子Toggle。
3.5 orientation:方向感知
orientation类型为Orientation('horizontal' | 'vertical'),默认'horizontal'。它同时影响两点:方向键焦点的移动方向(水平组用左右键、垂直组用上下键),以及根元素上的data-orientation数据属性取值。
3.6 onValueChange:变更回调
当组内按下状态变化时触发,签名如下:
onValueChange?: ( groupValue: Value[], eventDetails: ToggleGroup.ChangeEventDetails, ) => void;第一个参数是变更后的完整按下值数组;第二个参数是事件详情对象(详见下文"事件详情类型")。从源码可以看出,回调在状态写入之前被调用,并且如果eventDetails.isCanceled为真,组件会跳过内部状态更新(ToggleGroup.tsx),因此你可以在回调中通过cancel()拦截状态变更。
四、渲染定制:render、className 与 style
ToggleGroup继承自 Base UI 的BaseUIComponentProps<'div', ToggleGroupState>(见 ToggleGroup.tsx),因此支持三种渲染定制方式:
className:可以是普通字符串,也可以是接收ToggleGroup.State并返回字符串的函数,便于基于disabled/multiple/orientation状态做条件样式。官方 Tailwind 示例大量使用了这一点。style:普通样式对象或基于状态返回样式对象的函数,适合运行时动态计算样式。render:允许把默认的div替换为其他标签(如ul),或与另一个组件组合。可传入一个 ReactElement,或一个接收HTMLProps与ToggleGroup.State并返回元素的函数。
三种方式会在非 Toolbar 场景下统一透传给CompositeRoot,在 Toolbar 场景下则通过useRenderElement处理(ToggleGroup.tsx),保证任何定制都能与内部状态(如data-*属性)保持一致。
五、Data Attributes:无样式 CSS 的状态锚点
由于组件无内置样式,官方推荐通过数据属性编写 CSS。下表完整继承自 types.md/react/components/toggle-group/types.md#L26-L32):
| Attribute | Type | Description |
|---|---|---|
| data-orientation | 'horizontal' \| 'vertical' | Indicates the orientation of the toggle group. |
| data-disabled | - | Present when the toggle group is disabled. |
| data-multiple | - | Present when the toggle group allows multiple buttons to be in the pressed state at the same time. |
这三个属性的定义可在 ToggleGroupDataAttributes.ts 中直接找到,例如:
export const orientation = 'data-orientation'; export const disabled = 'data-disabled'; export const multiple = 'data-multiple';官方 Tailwind 示例(demos/hero/tailwind/index.tsx/react/components/toggle-group/demos/hero/tailwind/index.tsx))正是基于data-pressed等属性实现按下态样式切换:
data-pressed:bg-neutral-950>type ToggleGroupState = { /** Whether the component should ignore user interaction. */ disabled: boolean; /** * When `false` only one item in the group can be pressed. If any item in * the group becomes pressed, the others will become unpressed. * When `true` multiple items can be pressed. * @default false */ multiple: boolean; /** The orientation of the toggle group. */ orientation: Orientation; };它是className、style、render函数式用法中第二个参数的来源,也是 Base UI 状态驱动的样式方案的基础。
6.2 ToggleGroup.ChangeEventReason
type ToggleGroupChangeEventReason = 'none';目前该组件所有变更事件的reason固定为'none'。在源码中它被定义为typeof REASONS.none(ToggleGroup.tsx),复用自packages/react/src/internals/reasons,表明事件原因机制已就位,未来若引入更多触发来源可直接扩展该联合类型。
6.3 ToggleGroup.ChangeEventDetails
onValueChange的第二参数字段完整定义如下(摘自 types.md/react/components/toggle-group/types.md#L62-L81)):
type ToggleGroupChangeEventDetails = { /** The reason for the event. */ reason: 'none'; /** The native event associated with the custom event. */ event: Event; /** Cancels Base UI from handling the event. */ cancel: () => void; /** Allows the event to propagate in cases where Base UI will stop the propagation. */ allowPropagation: () => void; /** Indicates whether the event has been canceled. */ isCanceled: boolean; /** Indicates whether the event is allowed to propagate. */ isPropagationAllowed: boolean; /** The element that triggered the event, if applicable. */ trigger: Element | undefined; };cancel():调用后isCanceled变为true,组件会跳过内部状态更新(对应 ToggleGroup.tsx 的拦截逻辑),可用于实现"不允许取消最后一项"等业务规则。allowPropagation():在 Base UI 会主动阻止传播的场景下,手动放行事件冒泡。trigger:触发本次事件的 DOM 元素(若存在),便于定位用户点击的按钮。
6.4 外部类型 Orientation
Orientation是一个跨组件共享的外部类型(定义于packages/react/src/internals/types):
type Orientation = 'horizontal' | 'vertical';它同时被ToggleGroup、Toolbar、Tabs等方向敏感组件复用,因此传值方式在各组件间保持一致。
6.5 Canonical Types 命名映射
官方文档提供了"规范命名(Canonical)↔ 别名(Alias)"的映射表,规则为:当命名空间ToggleGroup已被导入时优先用 Canonical 写法,否则使用 Alias:
| Canonical | Alias |
|---|---|
ToggleGroup.State | ToggleGroupState |
ToggleGroup.Props | ToggleGroupProps |
ToggleGroup.ChangeEventReason | ToggleGroupChangeEventReason |
ToggleGroup.ChangeEventDetails | ToggleGroupChangeEventDetails |
在源码中这通过 TypeScript namespace 重新导出实现(ToggleGroup.tsx),两种写法类型完全等价。例如组件定义中的泛型签名ToggleGroup.Props<Value extends string>便采用了 Canonical 形式。
七、官方示例:单选与多选实战
7.1 单选:文本对齐工具条
官方 Hero 示例(demos/hero/tailwind/index.tsx/react/components/toggle-group/demos/hero/tailwind/index.tsx))演示了默认单选模式下的"文本对齐"工具栏,核心逻辑如下:
<ToggleGroup aria-label="Text alignment" defaultValue={['left']} className="flex gap-px p-px border border-neutral-950 dark:border-white" > <Toggle aria-label="Align left" value="left" className="...">…</Toggle> <Toggle aria-label="Align center" value="center" className="...">…</Toggle> <Toggle aria-label="Align right" value="right" className="...">…</Toggle> </ToggleGroup>注意三个关键点:aria-label为整组提供可访问名称;defaultValue={['left']}设置初始选中项;由于未传multiple,点击任意按钮会自动取消其余按钮。
7.2 多选:文本格式工具栏
Multiple 示例(demos/multiple/tailwind/index.tsx/react/components/toggle-group/demos/multiple/tailwind/index.tsx))模拟常见的"加粗/斜体/下划线"格式工具栏:
<ToggleGroup multiple defaultValue={['bold', 'italic']} aria-label="Text formatting options" className="flex gap-px p-px border border-neutral-950 dark:border-white" > <Toggle aria-label="Bold" value="bold" className="...">…</Toggle> <Toggle aria-label="Italic" value="italic" className="...">…</Toggle> <Toggle aria-label="Underline" value="underline" className="...">…</Toggle> </ToggleGroup>multiple开启后,bold与italic可以同时保持按下,这是文本编辑器中"组合样式"的典型交互。仓库同时提供了 CSS Modules 版本(demos/multiple/css-modules/)与 Tailwind 版本供对照学习。
八、源码实现解析:状态、Context 与 Toolbar 集成
深入阅读 ToggleGroup.tsx 的实现,可以归纳出四条关键实现路径:
- 状态管理:受控/非受控统一交给
useControlled(@base-ui/utils/useControlled);更新回调经useStableCallback包装,保证在依赖变化后仍能拿到最新groupValue,避免闭包过期。 - Context 下发:组件通过
ToggleGroupContext.Provider下发{ value, setGroupValue, disabled, isValueInitialized }(ToggleGroupContext.ts),子Toggle借此读取组状态、上报自己的按下事件,并在缺少value且组已初始化值时发出告警。 - Toolbar 集成:组件会主动探测上层的
ToolbarRootContext与ToolbarGroupContext(ToggleGroup.tsx)。当处于 Toolbar 内部时,焦点管理与禁用态继承自 Toolbar,渲染走useRenderElement;否则自行渲染CompositeRoot提供方向键与焦点循环。这解释了为什么在 Toolbar 内嵌套ToggleGroup时无需重复声明键盘导航能力。 - 无障碍语义:默认渲染
div并附加role="group",配合每个Toggle的aria-pressed状态(由子组件维护),整组对屏幕阅读器呈现为"可切换按钮组"。
九、测试验证:行为契约
ToggleGroup.test.tsx(共 605 行)为上述 API 行为提供了可执行的契约证明,关键断言包括:
- 无障碍角色:渲染后可通过
role="group"查询到根元素(#L18-L22)。 - 非受控按下状态:点击
value="one"的按钮后,该按钮aria-pressed变为true且带data-pressed,另一按钮仍为false(#L24-L53)。 - defaultValue 初始选中:
defaultValue={['two']}时第二个按钮初始即按下,点击其他按钮后互斥切换(#L55-L74)。 - Toggle 缺失 value 告警:组内
Toggle未传value且组已定义value/defaultValue时,控制台会输出精确的错误提示(#L98-L113)。 - 受控模式:通过
setProps重设value可同步更新 UI(#L116-L120起)。
这些测试既验证了 Props 的行为语义,也确认了aria-pressed、data-pressed等属性由Toggle侧负责输出,而ToggleGroup仅负责整组状态与焦点管理——二者职责划分清晰,值得在阅读源码时对照体会。
十、小结
ToggleGroup是 Base UI 中"分组状态 + 键盘导航 + 无障碍语义"三者结合的典型组件:value/defaultValue/onValueChange完成状态受控闭环,multiple决定单选或多选,loopFocus与orientation接管键盘体验,disabled支持与Toolbar联动的整组禁用,className/style/render提供无样式场景下的全部渲染定制入口,而data-orientation、data-disabled、data-multiple三个数据属性则让样式层能够纯粹基于状态编写。配合官方示例、源码与测试,你可以在此基础上快速落地自己的分段控件或格式工具栏。
【免费下载链接】base-uiUnstyled UI components for building accessible web apps and design systems. From the creators of Radix, Floating UI, and Material UI.项目地址: https://gitcode.com/GitHub_Trending/ba/base-ui
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考