Cadenza
EN

开关 —— 封装层自己组合的 Switch.Root + Switch.Thumb,box-only、没有 indeterminate,size 与 pending 是加的

Base UI 的 Switch.Root + Switch.Thumb,在 shadcn 的 base-nova 轨道里由封装层自己组合 而成 —— 类名照抄,加的只有 sizepending

它和 Checkbox 同形:根元素渲染一个 <span role="switch">, 旁边跟一个隐藏的 <input type="checkbox">。也就是说根元素就是那条轨道,别的什么都不是 (box-only),文字塞进 children 会被挤进轨道里 —— 所以文字走同级的 FieldLabel,由 htmlFor 指过来。

使用

import {
  Field,
  FieldContent,
  FieldDescription,
  FieldLabel,
  Switch,
} from '@gedatou/cadenza-ui'
<Field orientation="horizontal">
  <Switch id="notify" name="notify" />
  <FieldLabel htmlFor="notify">邮件通知</FieldLabel>
</Field>

标签

Field orientation="horizontal" + Switchid + FieldLabel htmlFor,一条线就够。

id 落在隐藏的那个 <input>(可见的轨道拿的是生成 id),所以一个 htmlFor 一条线管两件事:原生 <label for> 把点击转发给那个 input —— 这才是切换开关的那一下; Base UI 再把 label 的 id 反过来镜像到轨道上当 aria-labelledby —— 这才是屏幕阅读器 读到的名字。不需要第二个 aria-label,封装层也不用补点击接线。这条通道的来龙去脉写在 Checkbox 那页,两边完全一样;全库四条通道的总表在 Field

描述

带描述时用 FieldContent 把「标签 + 描述」装成一列,和 Checkbox 同一个写法 —— hero 就是这个形态:

<Field orientation="horizontal">
  <Switch id="notify" name="notify" defaultChecked />
  <FieldContent>
    <FieldLabel htmlFor="notify">排练提醒</FieldLabel>
    <FieldDescription>有新的排练安排时给你发一封邮件。</FieldDescription>
  </FieldContent>
</Field>

受控

checked / defaultChecked / onCheckedChange 三件套,和 Checkbox 同一套写法。

onCheckedChange 的第二参是真的 ChangeEventDetailsSwitchChangeEventDetails): reason 恒为 'none'(上游给开关只发这一种),而 cancel() 是认数的 —— 调了内部状态就不往前走,不是只把事件标记一下:

<Switch
  id="notify"
  checked={notify}
  onCheckedChange={(checked, eventDetails) => {
    if (locked) {
      eventDetails.cancel() // 内部状态不推进,开关留在原处
      return
    }
    setNotify(checked)
  }}
/>

表单

原生的,和 Checkbox 一样:给了 name,隐藏 input 在开着时提交 value(不传就是 "on"), 关着时提交 uncheckedValue —— 默认什么都不提交。开关渲染在 <form> 之外时用 form 指回表单 id。

进行中

拨动即提交是开关的语义,正好也是 round-trip 需要反馈的时刻——pending 标记「服务端还没确认」。

Button 的进行中同一套规矩:动作面的词是 pendingloading 属于内容面);开关保持可聚焦但不再响应(底下走 readOnly,表单控件现成的「能聚焦、拨不动」通道);视觉是 Spinner 转在 滑块圆点里——圆点在哪一侧、什么尺寸,Spinner 就跟到哪;aria-busydata-pendingpending 派生,调用方无法只设一半。

禁用

disabled 让开关不响应交互并降到半透明;readOnly 是另一档 —— 能聚焦,但拨不动。

两个都只管控件自己那一格。要让整列文字跟着变灰,按 Field 的状态约定在 Field 上手动挂 data-disabled

无效态

控件挂 aria-invalid,字段那一列挂 data-invalid

挂在哪效果
Switcharia-invalid轨道画出 destructive 边框与环
Fielddata-invalid整列文字变 destructive 色,见 Field

尺寸

sizeshadcn 加的,不是 Base UI 的 —— Switch.Root 上没有这个 prop。它镜像成 根元素的 data-size,轨道按它取宽高,滑块再从 group-data-[size=…] 取自己的尺寸和 位移:一个属性,两处生效。

什么时候用 Switch

行为上 Switch 和 Checkbox 只差一件事:Switch 没有 indeterminate —— 开关只有开和关, 没有第三态。剩下的差别全在这个可供性读起来是什么:

控件读作典型场景
Switch立即生效的开关设置页的「深色模式」「排练提醒」—— 拨一下就变了
Checkbox待提交的勾选「同意条款」、列表多选 —— 提交之后才算数

判断方法很直接:拨完就该看见效果的用 Switch;要等表单提交才生效、或者需要「部分选中」的, 用 Checkbox

状态与 className

className 通到的是 Base UI 的 slot,所以函数形态有效(state: SwitchState) => string | undefinedstyle 同样双形态。左列是挂在根元素上的 data-*(Tailwind 直接当变体写),右列是同一状态在函数形态里的名字。

data-*出现时机SwitchState 里的名字
data-checked / data-unchecked开 / 关,两者恒有其一checked
data-disableddisableddisabled
data-readonlyreadOnlyreadOnly
data-requiredrequiredrequired
data-size恒在,值是 default / sm—(shadcn 加的,不进 state)
data-pendingpending—(seam 派生,不进 state)

SwitchState 还带着 Field 那几个字段(valid / touched / dirty / filled / focused,对应 data-valid / data-touched 等)—— 但它们只在 Base UI 的 Field.Root 里才会动,而本库的 Field 是纯 DOM 的,不是它。要让整列文字 跟着控件变灰 / 变红,还是按 Field 那页的约定在 Field 上手动挂 data-disabled / data-invalid,见禁用无效态

根元素带 data-slot="switch",滑块带 data-slot="switch-thumb"pending 时 Spinner 渲染在滑块内,带 data-slot="spinner"),需要从外部定位时当 选择器用。

Props

顺序规则:必填 → 非受控默认值 → 受控值 → 回调 → 行为开关 → 外观 → className

Prop类型默认值说明
idstring落在隐藏 <input> 上,FieldLabel htmlFor 指这里(只有 nativeButtontrue 时才改落根元素)
namestring表单里标识这个字段
defaultCheckedbooleanfalse非受控初值
checkedboolean受控值
onCheckedChange(checked: boolean, eventDetails: SwitchChangeEventDetails) => void开关翻转时触发。reason 恒为 'none'cancel() 拦下这次变更
pendingboolean动作在途:可聚焦但不响应,Spinner 转在滑块圆点里,见进行中
disabledbooleanfalse不响应交互
readOnlybooleanfalse可聚焦,但拨不动
requiredbooleanfalse提交表单前必须打开
valuestring开着时提交的值;不给就是原生的 "on"(浏览器对无 value 的 checkbox 的行为,不是 Base UI 给的默认值)
uncheckedValuestring关着时提交的值;不传则关着时不提交任何值
formstring隐藏 input 归属的表单 id,开关渲染在表单外时用
inputRefReact.Ref<HTMLInputElement>拿到隐藏的 <input>
size'default' | 'sm''default'shadcn 的,不是 Base UI 的;镜像成 data-size
nativeButtonbooleanfalserender 换成原生 <button> 时置 true
renderReactElement | (props, state) => ReactElement替换根元素本身
classNamestring | ((state: SwitchState) => string | undefined)类名,双形态,见状态与 className
styleCSSProperties | ((state: SwitchState) => CSSProperties | undefined)同上,双形态
其余Base UI Switch.Root 的 props(span 的原生属性 + ref透传到轨道;aria-invalid 画出 destructive 边框与环

SwitchProps / SwitchState / SwitchChangeEventDetails 三个类型一并导出。