Base UI 的 Switch.Root + Switch.Thumb,在 shadcn 的 base-nova 轨道里由封装层自己组合
而成 —— 类名照抄,加的只有 size 和 pending。
它和 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" + Switch 的 id + 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 的第二参是真的 ChangeEventDetails(SwitchChangeEventDetails):
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 的进行中同一套规矩:动作面的词是
pending(loading 属于内容面);开关保持可聚焦但不再响应(底下走
readOnly,表单控件现成的「能聚焦、拨不动」通道);视觉是 Spinner 转在
滑块圆点里——圆点在哪一侧、什么尺寸,Spinner 就跟到哪;aria-busy 与
data-pending 由 pending 派生,调用方无法只设一半。
禁用
disabled 让开关不响应交互并降到半透明;readOnly 是另一档 —— 能聚焦,但拨不动。
两个都只管控件自己那一格。要让整列文字跟着变灰,按 Field
的状态约定在 Field 上手动挂 data-disabled。
无效态
控件挂 aria-invalid,字段那一列挂 data-invalid:
尺寸
size 是 shadcn 加的,不是 Base UI 的 —— Switch.Root 上没有这个 prop。它镜像成
根元素的 data-size,轨道按它取宽高,滑块再从 group-data-[size=…] 取自己的尺寸和
位移:一个属性,两处生效。
什么时候用 Switch
行为上 Switch 和 Checkbox 只差一件事:Switch 没有 indeterminate —— 开关只有开和关,
没有第三态。剩下的差别全在这个可供性读起来是什么:
判断方法很直接:拨完就该看见效果的用 Switch;要等表单提交才生效、或者需要「部分选中」的, 用 Checkbox。
状态与 className
className 通到的是 Base UI 的 slot,所以函数形态有效:
(state: SwitchState) => string | undefined;style 同样双形态。左列是挂在根元素上的
data-*(Tailwind 直接当变体写),右列是同一状态在函数形态里的名字。
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。
SwitchProps / SwitchState / SwitchChangeEventDetails 三个类型一并导出。