组件
切换按钮组 Toggle Group
将多个双态切换按钮组合为单选互斥(Single)或并列多选(Multiple)的分组控件。
基础用法
单选互斥模式(type="single")。在网格视图、舒适列表与紧凑列表之间互斥切换,选中背景会以弹簧曲线在选项之间平滑滑动:
Loading…
安装与引入
通过 CLI 自动添加组件,或手动复制源码至项目中:
pnpm dlx @wui-design/cli@latest add @wui/toggle-group安装基础依赖
pnpm add radix-ui class-variance-authority clsx tailwind-merge复制组件源码到
components/ui/toggle-group.tsx"use client"
import * as React from "react"
import { ToggleGroup as ToggleGroupPrimitive } from "radix-ui"
import { AnimatePresence, motion, useReducedMotion } from "motion/react"
import type { VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
import { toggleVariants } from "@/components/ui/toggle"
type ToggleGroupStyle = VariantProps<typeof toggleVariants>
type SelectionValue = string | string[] | undefined
interface ToggleGroupContextValue extends ToggleGroupStyle {
type: "single" | "multiple"
selected: string[]
layoutId: string
}
const ToggleGroupContext = React.createContext<ToggleGroupContextValue>({
type: "single",
selected: [],
layoutId: "wui-toggle-group",
})
const indicatorSpring = {
type: "spring",
stiffness: 520,
damping: 38,
mass: 0.7,
} as const
/**
* Mirrors the selected value(s) of a Radix toggle group (controlled or not) so
* items can render an animated indicator. Shared with `ToolbarGroup`.
*/
function useToggleGroupSelection({
value,
defaultValue,
onValueChange,
}: {
value?: SelectionValue
defaultValue?: SelectionValue
onValueChange?: (value: never) => void
}) {
const layoutId = React.useId()
const [internal, setInternal] = React.useState<SelectionValue>(defaultValue)
const current = value !== undefined ? value : internal
const selected = Array.isArray(current) ? current : current ? [current] : []
const handleValueChange = (next: string | string[]) => {
if (value === undefined) setInternal(next)
onValueChange?.(next as never)
}
return { layoutId, selected, handleValueChange }
}
export interface ToggleGroupIndicatorProps {
/** Whether the owning item is currently on. */
active: boolean
/** Shared layout id; pass it in single mode so the highlight slides between items. */
layoutId?: string
}
/**
* Background highlight for a toggle group item. With a `layoutId` it slides
* between items (single selection); without one, each item's highlight scales
* in and out on its own (multiple selection).
*/
function ToggleGroupIndicator({ active, layoutId }: ToggleGroupIndicatorProps) {
const reduceMotion = useReducedMotion()
const transition = reduceMotion ? { duration: 0 } : indicatorSpring
const className =
"pointer-events-none absolute inset-0 -z-10 rounded-[inherit] bg-accent"
if (layoutId) {
return active ? (
<motion.span
aria-hidden
data-slot="toggle-group-indicator"
layoutId={layoutId}
className={className}
transition={transition}
/>
) : null
}
return (
<AnimatePresence initial={false}>
{active ? (
<motion.span
key="indicator"
aria-hidden
data-slot="toggle-group-indicator"
className={className}
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.8 }}
transition={transition}
/>
) : null}
</AnimatePresence>
)
}
/** Item classes shared by `ToggleGroupItem` and `ToolbarToggleItem`. */
const toggleGroupItemClassName =
"relative isolate bg-transparent data-[state=on]:bg-transparent"
export type ToggleGroupProps = React.ComponentProps<
typeof ToggleGroupPrimitive.Root
> &
ToggleGroupStyle
/**
* 将一组双态按钮组织为单选或多选控件。单选模式下选中背景会在选项之间平滑滑动,
* 多选模式下每个选项的背景独立缩放淡入。
*/
function ToggleGroup({
className,
variant = "default",
size = "default",
children,
...props
}: ToggleGroupProps) {
const { layoutId, selected, handleValueChange } = useToggleGroupSelection(
props as Parameters<typeof useToggleGroupSelection>[0]
)
const rootProps = {
...props,
onValueChange: handleValueChange,
} as React.ComponentProps<typeof ToggleGroupPrimitive.Root>
return (
<ToggleGroupPrimitive.Root
data-slot="toggle-group"
data-variant={variant}
data-size={size}
className={cn("flex w-fit items-center gap-1", className)}
{...rootProps}
>
<ToggleGroupContext.Provider
value={{ variant, size, type: props.type, selected, layoutId }}
>
{children}
</ToggleGroupContext.Provider>
</ToggleGroupPrimitive.Root>
)
}
export interface ToggleGroupItemProps
extends
React.ComponentProps<typeof ToggleGroupPrimitive.Item>,
ToggleGroupStyle {}
/** ToggleGroup 中的单个可选项。 */
function ToggleGroupItem({
className,
variant,
size,
value,
children,
...props
}: ToggleGroupItemProps) {
const context = React.useContext(ToggleGroupContext)
return (
<ToggleGroupPrimitive.Item
data-slot="toggle-group-item"
value={value}
className={cn(
toggleVariants({
variant: variant ?? context.variant,
size: size ?? context.size,
}),
toggleGroupItemClassName,
className
)}
{...props}
>
<ToggleGroupIndicator
active={context.selected.includes(value)}
layoutId={
context.type === "single" ? `${context.layoutId}-indicator` : undefined
}
/>
{children}
</ToggleGroupPrimitive.Item>
)
}
export {
ToggleGroup,
ToggleGroupIndicator,
ToggleGroupItem,
toggleGroupItemClassName,
useToggleGroupSelection,
}
属性 Props
ToggleGroup
ToggleGroup 支持以下配置属性,并继承 Radix UI ToggleGroupPrimitive.Root 的全部 HTML 属性:
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| type | "single" | "multiple" | — | 选择模式。'single' 为互斥单选模式,'multiple' 为多选并存模式。 |
| value | string | string[] | — | 受控模式下的选中值。单选时为字符串,多选时为字符串数组。 |
| defaultValue | string | string[] | — | 非受控模式下的初始选中值。 |
| onValueChange | (value: string | string[]) => void | — | 选中项发生改变时触发的回调函数。单选返回 string,多选返回 string[]。 |
| variant | "default" | "outline" | "default" | 传递给子项目的统一视觉外观风格。 |
| size | "sm" | "default" | "lg" | "default" | 传递给子项目的统一尺寸密度。 |
| disabled | boolean | false | 是否禁用整组切换按钮。 |
| aria-label | string | — | 整组控件的无障碍描述名称。 |
| className | string | — | 应用于外层按钮组容器的额外 CSS 类名。 |
ToggleGroupItem
ToggleGroupItem 为按钮组内的单个可选项,继承 Radix UI ToggleGroupPrimitive.Item 的属性:
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| value | string | — | 当前子项的唯一稳定标识值。 |
| variant | "default" | "outline" | — | 覆盖外层组设定的单项视觉风格。 |
| size | "sm" | "default" | "lg" | — | 覆盖外层组设定的单项尺寸。 |
| disabled | boolean | false | 是否禁用当前特定子项。 |
| aria-label | string | — | 纯图标模式下必须提供的无障碍文本标签。 |
| className | string | — | 应用于单项按钮的额外 CSS 类名。 |
事件 Events
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| onValueChange | (value: string) => void (single) | (value: string[]) => void (multiple) | — | 当用户在单选或多选模式下切换选中项时触发,返回最新的值或值数组。 |
使用场景与设计规范
ToggleGroup 适用于同级别多项操作的聚合控制栏(如富文本工具栏、画布视口设备切换、工单多维状态筛选)。
- ToggleGroup vs RadioGroup vs CheckboxGroup:
- ToggleGroup(切换按钮组):以紧凑水平/网格按钮栏呈现,适合工具栏与高频筛选,视觉更加扁平轻快。
- RadioGroup(单选框组):标准表单录入,每个选项通常带有标题和详细文字解释。
- CheckboxGroup(复选框组):纵向或大面积表单复选,支持半选(Indeterminate)等深层状态。
- 单选 vs 多选场景:
type="single":视图模式切换(网格 / 列表)、对齐方式(左 / 中 / 右)。type="multiple":富文本样式(加粗 + 斜体 + 下划线)、复合标签筛选。
- 图标子项必须配备 ARIA 标签:在纯图标按钮组中,每个
ToggleGroupItem都应配备精准的aria-label。 - 选中动效:单选模式下,选中背景是一个共享布局的指示层(
data-slot="toggle-group-indicator"),会在选项之间滑动;多选模式下每个选项的指示层独立缩放淡入。每个ToggleGroup实例使用独立的布局 ID,同页多个实例互不干扰;系统开启“减少动态效果”时直接切换。 - 自定义选中色:选中背景由指示层绘制,请通过
className="[&>[data-slot=toggle-group-indicator]]:bg-primary/10"调整,而不是覆盖data-[state=on]:bg-*。
场景示例
富文本排版与格式工具栏
将单选对齐方式与多选文字修饰样式组合,构成紧凑完整的文本编辑器工具栏,并实时作用于下方段落:
Loading…
响应式画布设备视口切换
在设计器或页面预览中,通过单选切换按钮组在桌面端、平板和移动端视口间切换,预览画布宽度随之以弹簧过渡:
Loading…
状态标签多维筛选
多选模式配合数量提示,构建轻巧直观的工单或订单状态过滤器:
Loading…
多选格式样式
经典的多选字体样式控制:
Loading…
无障碍与交互 Accessibility
- ARIA 角色矩阵:
- 单选模式(
type="single"):容器挂载role="radiogroup",子项挂载role="radio"与aria-checked。 - 多选模式(
type="multiple"):容器挂载role="group",子项挂载role="button"与aria-pressed。
- 单选模式(
- 键盘导航:
- Tab:聚焦到切换按钮组。
- ← / →(或 ↑ / ↓):在单选模式下,方向键自动在同组按钮之间循环漫游切换焦点。
- Space / Enter:激活选定项。
- 高对比度焦点环:子项聚焦时具备高清晰度焦点环,支持暗色模式与系统高对比度模式。