wui
组件

切换按钮组 Toggle Group

将多个双态切换按钮组合为单选互斥(Single)或并列多选(Multiple)的分组控件。

第三方依赖 · radix-ui第三方依赖 · class-variance-authority第三方依赖 · motion

基础用法

单选互斥模式(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
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' 为多选并存模式。
valuestring | string[]—受控模式下的选中值。单选时为字符串,多选时为字符串数组。
defaultValuestring | string[]—非受控模式下的初始选中值。
onValueChange(value: string | string[]) => void—选中项发生改变时触发的回调函数。单选返回 string,多选返回 string[]。
variant"default" | "outline""default"传递给子项目的统一视觉外观风格。
size"sm" | "default" | "lg""default"传递给子项目的统一尺寸密度。
disabledbooleanfalse是否禁用整组切换按钮。
aria-labelstring—整组控件的无障碍描述名称。
classNamestring—应用于外层按钮组容器的额外 CSS 类名。

ToggleGroupItem

ToggleGroupItem 为按钮组内的单个可选项,继承 Radix UI ToggleGroupPrimitive.Item 的属性:

属性类型默认值说明
valuestring—当前子项的唯一稳定标识值。
variant"default" | "outline"—覆盖外层组设定的单项视觉风格。
size"sm" | "default" | "lg"—覆盖外层组设定的单项尺寸。
disabledbooleanfalse是否禁用当前特定子项。
aria-labelstring—纯图标模式下必须提供的无障碍文本标签。
classNamestring—应用于单项按钮的额外 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:激活选定项。
  • 高对比度焦点环:子项聚焦时具备高清晰度焦点环,支持暗色模式与系统高对比度模式。