跳转到内容

Switch 开关 ​

一项设置的开与关,切换后立即生效。

用法 ​

不传 checked 即为非受控,开关自行维护状态

组件结构 ​

加粗的是必需部件。

data-scope="switch":root · thumb · hidden-input · label · text

示例 ​

受控 ​

传入 checked 后由宿主决定,组件自身不再修改状态;变化意图经 checked-change 发出,写回后才落位

当前:开

禁用 ​

disabled 同时阻止指针与键盘,状态机收不到 TOGGLE

颜色 ​

tone 决定选中态轨道使用哪族颜色,因此这里都设为开

brandneutralsuccesswarningdangerinfo

尺寸 ​

size 同时缩放轨道与滑块,不写即默认档

小缺省大

事件 ​

checked-change 带一份 { checked },非受控时内部转移也照常触发一次

翻转 0 次 · 最近落到 (还没动过)

自定义颜色 ​

开态轨道、关态轨道与滑块各是一个组件令牌,语气档之外的配色写在行内

轨道内文案与滑块标记 ​

轨道的子节点全部由作者决定,data-state 同时写在轨道与滑块上

异步提交 ​

受控开关在回执到达前不落位;loading 使提交期呈现为处理中而非禁用:交互挂起、滑块显示加载、仍可聚焦

已关闭

形状 ​

轨道与滑块共用同一个形状令牌,在实例上覆盖一次两者一起变方

直角圆角缺省

随表单提交 ​

提供 name 后才生成表单影子:开启时才提交,值默认为 on,与原生复选框一致

设计指引 ​

何时使用 ​

  • 设置页内立即生效的开关(通知、深色模式、自动保存)。

何时不用 ​

  • 值需要随表单一起提交时,使用复选框,它是表单控件的原生语义。
  • 工具栏上的格式按钮使用切换按钮。

特性 ​

  • loading 表达在途并锁住再次切换:按钮保持可聚焦,以 aria-busy 和滑块内指示器报告状态;受控宿主仍可写回 checked 完成事务,失败时保持原值。loading 不伪装为 disabled。
  • readOnly 与 disabled 分开:只读仍可聚焦。
  • 轨道保持实体表单控件:未选中使用中性底和明确内边界,选中使用实心语气色,只读选中回到中性底;不使用 backdrop 或透明材质。
  • 滑块是 raised 抬起面:surface-raised 底 + border-default 描边 + raised 影,无顶光;静息即抬起,悬停不再升档,按住时沿行进方向拉长并在释放时回圆。loading、只读与禁用不产生按压反馈。
  • 滑块可以拖:横向拖过中点松手即切换,拖出两端越拉越沉,松手后滑块带着松手速度落到那一端;纵向划动留给页面滚动,不拖的点按照常切换。
  • 键盘聚焦环在明暗主题和开关两态都与轨道达到 3:1;RTL 会反转滑块行程与拖动方向,三尺寸与密度轴保持同一比例。
  • 减弱动效会取消按压拉伸并让 loading 圆环停转,以静止点线继续表达在途。

组合 ​

最佳实践 ​

  • 标签写设置本身(“邮件通知”),不写动作(“开启邮件通知”),开关的状态已经说明开或关。
  • 异步提交时使用 loading 并保持受控,不先切换再回滚。
  • 自定义轨道与滑块颜色时同时验证未选中边界、选中底和聚焦环;只换一支底色可能让暗色主题失去边界。

当前边界 ​

  • label 目前只直接获得 disabled 状态,loading / readonly 光标需由皮肤读取内部 root;后续应由连接层把两轴同步到 label,移除关系选择器并让所有基线浏览器得到同一反馈。
  • React / Vue 的紧凑 XhSwitch 把默认插槽固定为轨道外标签,没有暴露轨道内容或 thumb 插槽;只有 Web Components 的 Light DOM 能为 thumb 写作者内容。三端支持开关内文案或自定义标记应以独立部件 API 一起补齐。

反模式 ​

  • 开关切换后还需要点击“保存”,说明它应该是复选框。
  • 用开关表达两个并列选项(列表 / 网格)。

API 参考 ​

产物 ​

层值
自定义元素<xh-switch>
Vue 组件XhSwitch
组合式函数useSwitch
状态机switchMachine
皮肤@xihan-ui/styles/switch.css

Props ​

属性类型必填说明
checkedboolean
defaultCheckedboolean
disabledboolean
readOnlyboolean只读:不可切换,但仍可聚焦、仍参与提交,对比度不降低。
invalidboolean校验失败:只改变呈现,不阻止交互。
requiredboolean必填:随表单校验一起使用,只发无障碍属性,不自行拦截提交。
loadingboolean提交中:交互挂起、滑块转圈,但不呈现为禁用(仍可聚焦、对比度不降)。
namestring表单字段名;提供后 hidden-input 才带 name 并参与提交。
valuestring提交的值,默认 'on',与原生复选框一致。
toneTone语气:brand / neutral / success / warning / danger / info,决定选中态轨道使用哪族颜色。
sizeSize尺寸:sm / md / lg,决定轨道与滑块的几何档位。
onCheckedChange(details: SwitchCheckedChangeDetails) => voidchecked 变化意图回调;受控时是唯一出口,非受控时随内部转移一并通知。

事件 ​

自定义元素将载荷放在 detail;Vue 使用同名 emit。

事件载荷说明
checked-changeSwitchCheckedChangeDetailschecked 状态变化;detail 为 { checked: boolean }

React 适配器 props ​

只列各组件自己声明的那些:继承自 ComponentPropsWithRef 的 DOM 属性不在其中,根组件上与上面 Props 表同名的也不重复列。Vue 的对应物是上面的插槽表。

React 组件属性类型必填说明
XhSwitchchildrenReactNode

状态 ​

公开状态写入 data-state。

部件取值
root'checked' | 'unchecked'
thumb'checked' | 'unchecked'
label'checked' | 'unchecked'
text'checked' | 'unchecked'

以下名称仅用于内部状态机。

状态:off · on

事件:TOGGLE · CONTROLLED.ON · CONTROLLED.OFF · FORM.RESET · DRAG.START · DRAG.MOVE · DRAG.END · CLICK.SWALLOW · PRESS.START · PRESS.END

判据:isCheckedControlled · defaultsToChecked · canPress

connect API ​

getXxxProps() 返回对应部件的宿主属性。

成员类型说明
checkedboolean
loadingboolean提交中。
setChecked(next: boolean) => void
getRootProps() => T['button']
getThumbProps() => T['element']
getHiddenInputProps() => T['input']表单影子:开启后才提交。提供 name 后才带 name,未提供时不参与提交。
getLabelProps() => T['label']包裹轨道与文字的 <label>:点击文字即切换,轨道的可及名来自文字。只在带文字时渲染。
getTextProps() => T['element']轨道旁的文字。

无障碍 ​

键盘 ​

规格出处:W3C APG

按键生效条件行为
Space / Enterfocus in root, not disabled切换 checked 状态
Space / Enterheld in root, not disabled, not loading, not readOnly按住期间投影 data-pressed,与指针 :active 同一副按压面;抬起或失焦撤下,按住途中转入禁用、提交中或只读也撤下。与开关态互相独立

ARIA ​

以下属性由 connect 生成。

部件属性值
rootaria-busy'true' | undefined
rootaria-checked'true' | 'false'
rootaria-invalid'true' | 'false'
rootaria-readonly'true' | 'false'
rootaria-required'true' | 'false'
rootrole'switch'

样式参考 ​

皮肤 ​

@xihan-ui/styles/switch.css 使用 [data-scope="switch"][data-part="root"] 部件选择器,位于 xihan.components 层。覆盖样式使用 xihan.overrides。

forced-colors: active 下另有一套规则:颜色交给系统,边框与状态标记改用系统色关键字。

数据属性 ​

由 connect 生成;条件不成立时不输出无值属性。

部件属性值
rootdata-disabled''(条件成立时才出现)
rootdata-dragging''(条件成立时才出现)
rootdata-invalid''(条件成立时才出现)
rootdata-loading''(条件成立时才出现)
rootdata-pressed''(条件成立时才出现)
rootdata-readonly''(条件成立时才出现)
rootdata-required''(条件成立时才出现)
rootdata-sizeprops.size
rootdata-state'checked' | 'unchecked'
rootdata-toneprops.tone
rootdata-xh-action-control''
rootdata-xh-action-display'always'
rootdata-xh-action-profile'text'
rootdata-xh-action-sizeprops.size
rootdata-xh-action-variant'outline'
thumbdata-animating''(条件成立时才出现)
thumbdata-disabled''(条件成立时才出现)
thumbdata-dragging''(条件成立时才出现)
thumbdata-loading''(条件成立时才出现)
thumbdata-state'checked' | 'unchecked'
labeldata-disabled''(条件成立时才出现)
labeldata-sizeprops.size
labeldata-state'checked' | 'unchecked'
textdata-disabled''(条件成立时才出现)
textdata-state'checked' | 'unchecked'

CSS 变量 ​

本组件公开覆盖槽由独立皮肤的实际消费位生成;默认来源、作用部件和状态均与 CSS 同源。

变量部件CSS 属性状态默认来源说明
--xh-switch-bgroot--xh-ink-surface
background-color
default
disabled
focus-visible
hover
is(:active, [data-pressed])
loading
not([data-disabled])
not([data-loading])
pressed
readonly
xh-ink-surface
--xh-bg-subtle-activeswitch 的 root 部件 --xh-ink-surface、background-color 覆盖槽。
--xh-switch-bg-checkedroot--xh-ink-surface
background-color
disabled
focus-visible
hover
is(:active, [data-pressed])
loading
not([data-disabled])
not([data-loading])
pressed
readonly
state=checked
xh-ink-surface
--xh-_switch-accentswitch 的 root 部件 --xh-ink-surface、background-color 覆盖槽。
--xh-switch-bg-checked-pressedrootbackground-colordisabled
is(:active, [data-pressed])
loading
not([data-disabled])
not([data-loading])
pressed
state=checked
--xh-_tone-activeswitch 的 root 部件 background-color 覆盖槽。
--xh-switch-bg-checked-readonlyroot--xh-ink-surface
background-color
disabled
focus-visible
hover
is(:active, [data-pressed])
loading
not([data-disabled])
not([data-loading])
pressed
readonly
state=checked
xh-ink-surface
--xh-bg-subtle-activeswitch 的 root 部件 --xh-ink-surface、background-color 覆盖槽。
--xh-switch-bg-disabledroot--xh-ink-surface
background-color
disabled
focus-visible
hover
is(:active, [data-pressed])
loading
not([data-disabled])
not([data-loading])
pressed
readonly
xh-ink-surface
--xh-bg-subtleswitch 的 root 部件 --xh-ink-surface、background-color 覆盖槽。
--xh-switch-bg-pressedrootbackground-colordisabled
is(:active, [data-pressed])
loading
not([data-disabled])
not([data-loading])
pressed
--xh-_switch-track-bgswitch 的 root 部件 background-color 覆盖槽。
--xh-switch-borderrootbox-shadowcontrast=more
default
disabled
focus-visible
hover
is(:active, [data-pressed])
loading
not([data-disabled])
not([data-loading])
pressed
state=unchecked
where([data-contrast='more'])
--xh-border-control
--xh-border-strong
switch 的 root 部件 box-shadow 覆盖槽。
--xh-switch-border-checkedrootbox-shadowdisabled
focus-visible
hover
is(:active, [data-pressed])
loading
not([data-disabled])
not([data-loading])
pressed
state=checked
--xh-_switch-accentswitch 的 root 部件 box-shadow 覆盖槽。
--xh-switch-border-checked-readonlyrootbox-shadowcontrast=more
disabled
focus-visible
hover
is(:active, [data-pressed])
loading
not([data-disabled])
not([data-loading])
pressed
readonly
state=checked
where([data-contrast='more'])
--xh-border-control
--xh-border-strong
switch 的 root 部件 box-shadow 覆盖槽。
--xh-switch-border-disabledrootbox-shadowdisabled
focus-visible
hover
is(:active, [data-pressed])
loading
not([data-disabled])
not([data-loading])
pressed
--xh-border-defaultswitch 的 root 部件 box-shadow 覆盖槽。
--xh-switch-border-invalidrootbox-shadowdisabled
focus-visible
hover
invalid
is(:active, [data-pressed])
loading
not([data-disabled])
not([data-loading])
pressed
--xh-border-invalidswitch 的 root 部件 box-shadow 覆盖槽。
--xh-switch-fgrootcolordefault
disabled
focus-visible
hover
is(:active, [data-pressed])
loading
not([data-disabled])
not([data-loading])
pressed
--xh-fg-defaultswitch 的 root 部件 color 覆盖槽。
--xh-switch-fg-checkedrootcolordisabled
focus-visible
hover
is(:active, [data-pressed])
loading
not([data-disabled])
not([data-loading])
pressed
state=checked
--xh-_tone-onswitch 的 root 部件 color 覆盖槽。
--xh-switch-fg-checked-readonlyrootcolordisabled
focus-visible
hover
is(:active, [data-pressed])
loading
not([data-disabled])
not([data-loading])
pressed
readonly
state=checked
--xh-fg-defaultswitch 的 root 部件 color 覆盖槽。
--xh-switch-fg-disabledrootcolordisabled
focus-visible
hover
is(:active, [data-pressed])
loading
not([data-disabled])
not([data-loading])
pressed
--xh-fg-disabledswitch 的 root 部件 color 覆盖槽。
--xh-switch-label-fglabelcolordefault--xh-fg-defaultswitch 的 label 部件 color 覆盖槽。
--xh-switch-label-fg-disabledlabelcolordisabled--xh-fg-subtleswitch 的 label 部件 color 覆盖槽。
--xh-switch-label-font-sizelabelfont-sizedefault--xh-_switch-label-font-sizeswitch 的 label 部件 font-size 覆盖槽。
--xh-switch-label-gaplabelgapdefault--xh-control-gap-mdswitch 的 label 部件 gap 覆盖槽。
--xh-switch-label-leadinglabelline-heightdefault--xh-leading-normalswitch 的 label 部件 line-height 覆盖槽。
--xh-switch-loading-durationthumbanimationloading--xh-motion-loop-spinswitch 的 thumb 部件 animation 覆盖槽。
--xh-switch-loading-fgthumbborder-block-start-color
border-color
@media (prefers-reduced-motion: reduce)
@media print
loading
motion=reduce
where([data-motion='reduce'])
--xh-_switch-accentswitch 的 thumb 部件 border-block-start-color、border-color 覆盖槽。
--xh-switch-radiusrootborder-radiusdefault--xh-shape-pillswitch 的 root 部件 border-radius 覆盖槽。
--xh-switch-thumbthumbbackgrounddefault--xh-bg-surface-raisedswitch 的 thumb 部件 background 覆盖槽。
--xh-switch-thumb-borderthumbborderdefault--xh-border-defaultswitch 的 thumb 部件 border 覆盖槽。
--xh-switch-thumb-fgthumbcolordefault--xh-fg-defaultswitch 的 thumb 部件 color 覆盖槽。
--xh-switch-thumb-fg-disabledroot
thumb
colordisabled--xh-fg-disabledswitch 的 root、thumb 部件 color 覆盖槽。
--xh-switch-thumb-press-stretchroot
thumb
inline-size
translate
disabled
is(:active, [data-pressed])
loading
not([data-disabled])
not([data-loading])
not([data-readonly])
pressed
readonly
state=checked
--xh-motion-distance-smswitch 的 root、thumb 部件 inline-size、translate 覆盖槽。
--xh-switch-thumb-radiusthumbborder-radiusdefault--xh-shape-circleswitch 的 thumb 部件 border-radius 覆盖槽。
--xh-switch-thumb-shadowthumbbox-shadowdefault--xh-elevation-raisedswitch 的 thumb 部件 box-shadow 覆盖槽。
--xh-switch-thumb-shadow-disabledroot
thumb
box-shadowdisablednoneswitch 的 root、thumb 部件 box-shadow 覆盖槽。
--xh-switch-thumb-shadow-pressedroot
thumb
box-shadowdisabled
is(:active, [data-pressed])
loading
not([data-disabled])
not([data-loading])
not([data-readonly])
pressed
readonly
noneswitch 的 root、thumb 部件 box-shadow 覆盖槽。
--xh-switch-thumb-shadow-readonlyroot
thumb
box-shadowreadonlynoneswitch 的 root、thumb 部件 box-shadow 覆盖槽。

动效 ​

动效角色:按压 · 状态 · 切换 · 循环(见动效规范)。

可覆盖的动效槽:--xh-switch-loading-duration · --xh-switch-thumb-press-stretch。

共享关键帧 xh-spin 由 family/motion.css 提供,皮肤 @import 它,单独引入仍成立;box-shadow · inline-size · translate 走 transition 过渡。时长与缓动读动效令牌,改令牌即改全局节奏。

prefers-reduced-motion: reduce 下本组件另有降级规则。

响应式 ​

皮肤另按输入能力分档:pointer: coarse:同一份皮肤在触屏与带指针的设备上不一样,与视口宽度无关。

RTL ​

皮肤用逻辑属性排布(inline-start 一族),dir="rtl" 下自动镜像;另有按 dir 分支的规则。

Released under The MIT License