跳至正文
组件

Toggle Switch方条开关

整条可点的开关,打开时铺上淡色、圆形手柄滑到右端,与数值滑块同一套高度。

正在加载…

安装

pnpm dlx shadcn@latest add https://yiui.dev/r/toggle-switch.json
会连带安装 本站条目 grid-layout、flow-diagram。已存在的文件会询问是否覆盖。

用法

components/autosave-switch.tsx
import { useState } from "react"
import { ToggleSwitch } from "@/components/ui/toggle-switch/toggle-switch"

export function AutosaveSwitch() {
  const [autosave, setAutosave] = useState(true)

  return (
    <ToggleSwitch
      label="自动保存"
      description={autosave ? "已开启" : "已关闭"}
      tone="blue"
      checked={autosave}
      onCheckedChange={setAutosave}
    />
  )
}

整条方条都是开关,不用对准右侧的小轨道。checked / defaultChecked / onCheckedChange 直接来自 Base UI 的 Switch.Root,受控与非受控都按这一套写。

外观

  • 关:bg-muted/60 的淡底、细边框,文字是次要色;右侧是一个灰色胶囊轨道,圆形手柄停在左端。
  • 开:整条从左往右铺上一层淡色(toggle-switch-fill 用 scale-x 展开,和数值滑块的已选填充同一种淡),轨道变成实色,手柄滑到右端,文字提到前景色。

tone 决定铺色(--flow-tone 的 15%,暗色下 25%)与打开时轨道的颜色;不传时用前景色,得到最中性的一档。手柄直径随尺寸档变化:bar 为 12 / 14 / 16px,compact 为 8 / 10 / 12px,整条高度与数值滑块对齐。

<ToggleSwitch label="高亮当前行" defaultChecked tone="violet" />

高度

variant 与 size 的对照表和数值滑块一致,同一行放控件时不会参差:

variantsmdefaultlg
bar28px32px36px
compact18px22px24px

compact 把字号收到 text-xs,几行叠起来也不占地方,适合调整面板与密排的设置页。

说明文字

description 渲染在标签右侧、字号字重都小一档,通常放当前状态或生效范围(「已开启」「邮件」「需开启同步」)。它是按钮内容的一部分,所以读屏会把标签与说明连起来念;只想念标签时传 aria-label。

<ToggleSwitch label="每周摘要" description="邮件" />

示例

密排

variant="compact" 收成 22px 高,六条加上 gap-1 也只占半张卡。密排时不带 description,一行只有一个标签,视线不会被切断。

正在加载…

禁用态

总开关关着时,下面三项 disabled:整条淡到 50%,不响应点击与键盘。打开总开关,三项立刻可以操作;description 在同一个位置换成状态文案,说明为什么点不动。

正在加载…

API

ToggleSwitch

除上表外,其余属性直接交给 Base UI 的 Switch.Root:checked、defaultChecked、onCheckedChange(checked, eventDetails)、disabled、name、required、aria-label 等。render、nativeButton、children 被移除了——组件固定渲染成原生 <button type="button">。

toggleSwitchVariants

cva 生成的根元素样式函数,有 size 与 variant 两个变体,高度通过根元素上的 --toggle-switch-height 变量传给内部。

const toggleSwitchVariants: (props?: {
  size?: ToggleSwitchSize
  variant?: ToggleSwitchVariant
}) => string

类型

type ToggleSwitchSize = "sm" | "default" | "lg"

type ToggleSwitchVariant = "bar" | "compact"

type ToggleSwitchProps = Omit<
  Switch.Root.Props,
  "render" | "nativeButton" | "children"
> & {
  /** 标签,也是开关的可访问名称 */
  label: React.ReactNode
  /** 标签右侧的补充说明(如当前状态「已开启」) */
  description?: React.ReactNode
  variant?: ToggleSwitchVariant
  size?: ToggleSwitchSize
  tone?: FlowTone
  modified?: boolean
}

data-slot

元素data-slot
根元素toggle-switch
打开时的铺色toggle-switch-fill
标签toggle-switch-label
说明文字toggle-switch-description
小轨道toggle-switch-track
手柄toggle-switch-thumb

根元素上还有:data-checked(Base UI 给的选中状态)、data-disabled、data-modified,以及始终存在的 data-variant(bar / compact)。

可访问性

  • 根元素是原生 <button role="switch">,开关状态由 aria-checked 表达;空格键与回车键都能切换,Tab 进入。
  • 整条都是可点区域,命中区就是那个 28 / 32 / 36px 高的方条,比常见的滑块式开关大得多。
  • 关闭时文字是次要色、打开时是前景色,铺色与手柄位置也同时变化——状态不只靠颜色。
  • 可访问名称来自按钮内的文字,label 与 description 会连起来念。想让读屏只念标签,传 aria-label。
  • 禁用时整条淡到 50%、光标变 not-allowed,并且从 Tab 顺序里移除(原生 disabled)。

注意事项

  • 手柄的位移是固定的(translate-x-4.5),跟着固定的轨道宽度走;RTL 下方向反转。
  • 铺色用 scale-x 展开,铺的是整条而不是轨道,视觉上像「点亮」而不是「拨动」。系统开启「减少动态效果」时铺色与手柄都瞬时切换。
  • 组件是全宽元素(w-full),放进窄容器时由容器决定宽度;标签与说明都是 truncate,过长时截断而不是换行。
  • tone 的颜色变量由 flow-diagram 条目提供,安装时会一并拉取。

源码

components/ui/toggle-switch/toggle-switch.tsx
"use client"

import * as React from "react"
import { Switch } from "@base-ui/react/switch"
import { cva } from "class-variance-authority"

import { cn } from "@/lib/utils"
import { flowToneStyle, type FlowTone } from "@/components/ui/flow-diagram/tone"

/**
 * 整条方条都是开关:左侧标签,右侧一个胶囊轨道和圆形手柄。
 * 打开时整条从左往右铺上淡色,轨道变成实色,手柄滑到右端。
 * 高度表与数值滑块一致(bar 28 / 32 / 36px,compact 18 / 22 / 24px)。
 */
const toggleSwitchVariants = cva(
  "group/toggle-switch relative flex h-(--toggle-switch-height) w-full min-w-0 cursor-pointer items-center justify-between gap-3 overflow-hidden rounded-md border border-edge bg-muted/60 px-3 text-start text-muted-foreground outline-none select-none transition-colors hover:text-foreground focus-visible:ring-3 focus-visible:ring-ring/50 data-[checked]:text-foreground data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50",
  {
    variants: {
      size: {
        sm: "text-xs",
        default: "text-sm",
        lg: "text-sm",
      },
      variant: {
        bar: "",
        compact: "text-xs",
      },
    },
    compoundVariants: [
      {
        variant: "bar",
        size: "sm",
        class:
          "[--toggle-switch-height:1.75rem] [--toggle-switch-knob:0.75rem]",
      },
      {
        variant: "bar",
        size: "default",
        class: "[--toggle-switch-height:2rem] [--toggle-switch-knob:0.875rem]",
      },
      {
        variant: "bar",
        size: "lg",
        class: "[--toggle-switch-height:2.25rem] [--toggle-switch-knob:1rem]",
      },
      {
        variant: "compact",
        size: "sm",
        class:
          "[--toggle-switch-height:1.125rem] [--toggle-switch-knob:0.5rem]",
      },
      {
        variant: "compact",
        size: "default",
        class:
          "[--toggle-switch-height:1.375rem] [--toggle-switch-knob:0.625rem]",
      },
      {
        variant: "compact",
        size: "lg",
        class: "[--toggle-switch-height:1.5rem] [--toggle-switch-knob:0.75rem]",
      },
    ],
    defaultVariants: {
      size: "default",
      variant: "bar",
    },
  },
)

type ToggleSwitchSize = "sm" | "default" | "lg"
type ToggleSwitchVariant = "bar" | "compact"

type ToggleSwitchProps = Omit<
  Switch.Root.Props,
  "render" | "nativeButton" | "children"
> & {
  /** 标签,也是开关的可访问名称 */
  label: React.ReactNode
  /** 标签右侧的补充说明(如当前状态「已开启」);不传时不显示 */
  description?: React.ReactNode
  /**
   * 高度档:`bar` 与数值滑块默认同高,`compact` 是面板里用的中等高度
   * @default "bar"
   */
  variant?: ToggleSwitchVariant
  /**
   * 尺寸档
   * @default "default"
   */
  size?: ToggleSwitchSize
  /** 色调:打开时的铺色与手柄取 `--flow-tone`;不传时用前景色 */
  tone?: FlowTone
  /**
   * 标记为「已改动」:标签改用前景色,根元素带 `data-modified`
   * @default false
   */
  modified?: boolean
}

/** 方条开关:基于 Base UI Switch,渲染为原生 `button`,带 `role="switch"`。 */
function ToggleSwitch({
  label,
  description,
  variant = "bar",
  size = "default",
  tone,
  modified = false,
  className,
  style,
  ...props
}: ToggleSwitchProps) {
  const hasTone = tone !== undefined

  return (
    <Switch.Root
      {...props}
      nativeButton
      render={<button type="button" />}
      data-slot="toggle-switch"
      data-variant={variant}
      data-modified={modified ? "" : undefined}
      className={cn(
        toggleSwitchVariants({ size, variant }),
        "data-[modified]:text-foreground",
        className,
      )}
      style={{ ...flowToneStyle(tone), ...style }}
    >
      {/* 打开时的铺色:从左往右展开,和滑块的已选填充同一种淡色 */}
      <span
        aria-hidden="true"
        data-slot="toggle-switch-fill"
        className={cn(
          "absolute inset-0 origin-left scale-x-0 transition-transform duration-200 ease-out group-data-[checked]/toggle-switch:scale-x-100 motion-reduce:transition-none rtl:origin-right",
          hasTone
            ? "bg-(--flow-tone)/15 dark:bg-(--flow-tone)/25"
            : "bg-foreground/5 dark:bg-foreground/10",
        )}
      />
      <span
        data-slot="toggle-switch-label"
        className="relative flex min-w-0 items-baseline gap-2 font-medium"
      >
        <span className="truncate">{label}</span>
        {description !== undefined ? (
          <span
            data-slot="toggle-switch-description"
            className="truncate font-normal text-muted-foreground"
          >
            {description}
          </span>
        ) : null}
      </span>
      {/* 胶囊轨道 + 圆形手柄:最容易一眼读懂的开关形态;打开时轨道换成实色,手柄滑到另一端 */}
      <span
        aria-hidden="true"
        data-slot="toggle-switch-track"
        className={cn(
          "relative flex h-[calc(var(--toggle-switch-knob)+4px)] w-[calc(var(--toggle-switch-knob)*2+4px)] shrink-0 items-center rounded-full bg-foreground/15 p-0.5 transition-colors duration-200 motion-reduce:transition-none dark:bg-foreground/20",
          hasTone
            ? "group-data-[checked]/toggle-switch:bg-(--flow-tone)"
            : "group-data-[checked]/toggle-switch:bg-foreground",
        )}
      >
        <Switch.Thumb
          data-slot="toggle-switch-thumb"
          className="size-(--toggle-switch-knob) rounded-full bg-background shadow-sm transition-transform duration-200 ease-out data-[checked]:translate-x-(--toggle-switch-knob) motion-reduce:transition-none rtl:data-[checked]:-translate-x-(--toggle-switch-knob)"
        />
      </span>
    </Switch.Root>
  )
}

export { ToggleSwitch, toggleSwitchVariants }
export type { ToggleSwitchProps, ToggleSwitchSize, ToggleSwitchVariant }