Base UI 的 Toggle 装进 shadcn 的 base-nova 皮肤。封装层没有加行为 —— 每个 prop
都是一次普通展开就到底下的部件;它只做两件事:把按下和悬停在视觉上分开
(见状态与 className),以及在类型上动两处 —— 把 className
窄化成字符串,并保住 value 那个 Value 泛型不塌成 string。
它是一个真的 <button aria-pressed>:根元素就是那颗按钮,键盘激活(Enter /
Space)、disabled、hover / focus-visible 伪类全部是原生的。
使用
import { Toggle } from '@gedatou/cadenza-ui'<Toggle aria-label="加粗" defaultPressed>
<IconBold />
</Toggle>标签
名字走 children,因为按钮的可访问名就是它自己的内容 —— 所以这里
没有 Checkbox / Switch
那条 id + FieldLabel htmlFor 的通道,封装层也不需要提供:那两个是 box-only 的
(根元素是方块 / 轨道,文字塞不进去,只能挂在同级的 label 上),而 Toggle 的文字本来
就在按钮里。有可见文字就直接写进 children,图标和文字并排也一样(hero 里第三颗
就是)。
只有图标、没有可见文字时补 aria-label —— 和 Button 的
icon-* 档同一条规矩。
反过来说,别给它挂 FieldLabel:那会盖掉按钮自己的文字当无障碍名,
Field 那页的通道表也因此不收 Toggle。
变体与尺寸
variant 和 size 是 shadcn 的 cva 旋钮,不是 Base UI 的 —— Toggle 上没有这两个
prop。三档高度分别是 h-7 / h-8 / h-9,并各配一个同尺寸的 min-w-*,所以纯图标
形态天然是方的。图标自己不用调尺寸:cva 里给未显式指定尺寸的 svg 兜了
size-4(size="sm" 时是 size-3.5)。
两个 variant 的差别在没按下的时候:default 是全透明的,outline 则不论按没按都有
一圈边框。
toggleVariants 也一并导出,需要把别的元素画成 toggle 的样子时直接调用。
受控
受控三件套是 pressed / defaultPressed / onPressedChange。回调的第二参是真的
ChangeEventDetails(导出名 ToggleChangeEventDetails):reason 恒为 'none'
(上游给 toggle 就只派了这一种,别指望靠它分辨来源),cancel() 是认数的 ——
调了它,组件内部那份状态就不推进:
<Toggle
aria-label="加粗"
pressed={bold}
onPressedChange={(pressed, eventDetails) => {
if (locked) {
eventDetails.cancel() // 内部状态不推进,按钮留在原处
return
}
setBold(pressed)
}}
/>受控时显示的本来就是你的 pressed,cancel() 拦下的是组件内部那份看不见的状态;
只给 defaultPressed 时,那份状态就是显示的那份,于是 cancel() 直接把这一下按压
拦住。这条和 Checkbox / Switch 完全同源。
什么时候用 Toggle
单个 Toggle 是按钮,不是表单字段:它没有 name,value 也不渲染到 DOM 上,
提交表单时什么都不带;Base UI 还把 type 写死成 "button"(type 和 form 传了
都不生效),所以放进 <form> 里点它也不会误提交。
判断方法:这一下改的是当前视图的模式,而且状态不会跟着表单走,用 Toggle;状态属于
一张表单(要序列化、要 required、可能要半选),用 Checkbox;是一条设置开关,用 Switch。
若干个相关的 toggle 共享一个值时用 ToggleGroup ——
value 就是在那里派上用场的(它标识这颗 toggle 在组的 value 数组里的身份)。
状态与 className
className 只收字符串:vendored 层把它灌进 cva,函数会被静默丢掉 —— 元素照样
拿到变体类,只有你写的那一份凭空消失。类型因此把这条路封了(和
Button 同一条路、同一处窄化)。
要按状态换样式,用 Base UI 写在按钮上的 data-*,Tailwind 直接当变体写:
<Toggle aria-label="加粗" className="data-pressed:text-primary">
<IconBold />
</Toggle>(ToggleState 类型照常导出,但因为 className 是字符串,这里拿不到函数形态的出口。)
悬停、按下瞬间、焦点没有 data 属性 —— 原生 <button> 上直接用 hover: / active: /
focus-visible: / disabled: 伪类,覆盖全部输入模态。根元素带 data-slot="toggle",
需要从外部定位时当选择器用。
按下和悬停不能长一个样,这是封装层唯一的一笔。 base-nova 两边都画 bg-muted,
而这个主题里 --muted / --accent / --secondary 是同一个值,换 token 分不开。
所以封装层:没按下时的 hover 降成半强度(同变体同 twMerge 组,替换掉 vendored 的
hover:bg-muted 而不是叠加);按下保持实心,并用 data-pressed:hover: 重述一遍,
免得已按下的按钮被那层淡色冲掉;按下另加一圈 inset ring —— 灰阶里单靠填充强度只差
约 1.5% 亮度,这圈环才是一眼能看出来的那一下,而且调用方改掉背景色它也还在。
Props
顺序规则:必填 → 非受控默认值 → 受控值 → 回调 → 行为开关 → 外观 → className。
ToggleProps / ToggleState / ToggleChangeEventDetails 三个类型与
toggleVariants 一并导出。