开关Switch
即时启停一项配置或业务状态(启用通知、公开数据、自动同步)
数据录入 规格 29 · 开关 Switch 6 端实现 触摸优先
<!-- ① 令牌:PC 令牌 + 移动端 --kole-m-* 合成单文件,引一次 -->
<link rel="stylesheet" href="kole-ui/mobile/tokens.css">
<!-- ② 本组件样式(全量则用 kole-ui/mobile/components/index.css) -->
<link rel="stylesheet" href="kole-ui/mobile/components/mobile-switch.css">
<!-- ③ 结构照抄下方任一演示块(类名与 6 端实现一致) -->
演示
每个演示都是真实渲染:预览帧加载 frameworks-mobile/Switch.html?demo=<id>(只显示该演示块),代码是该演示块在演示页里的原文,可复制。全部演示同屏可看 演示页 ↗。
01 组件类型
整行可点:开关本体只有 48×28,单点本体手指点不中。
查看代码(演示页原文 · 10 行)
<section class="demo-block" data-demo="basic">
<p class="demo-label">基础用法(整行可点:开关本体只有 48×28,单点本体手指点不中)</p>
<div class="demo-box">
<button class="kole-m-switch is-on" type="button" role="switch" aria-checked="true" id="sw-product"
data-assert="switch-basic" data-behavior="click-toggles-class:#sw-product|is-on">
<span class="kole-m-switch__track" aria-hidden="true"><span class="kole-m-switch__knob"></span></span>
<span class="kole-m-switch__text">商品公开可见</span>
</button>
</div>
</section>default 轨道 48×28;small 轨道 40×22 用于紧凑表单。
查看代码(演示页原文 · 13 行)
<section class="demo-block" data-demo="size">
<p class="demo-label">尺寸两档(size=default 48×28 / size=small 40×22,紧凑表单用)</p>
<div class="demo-box" data-assert="switch-size">
<button class="kole-m-switch is-on" type="button" role="switch" aria-checked="true">
<span class="kole-m-switch__track" aria-hidden="true"><span class="kole-m-switch__knob"></span></span>
<span class="kole-m-switch__text">default(48×28)</span>
</button>
<button class="kole-m-switch kole-m-switch--small is-on" type="button" role="switch" aria-checked="true">
<span class="kole-m-switch__track" aria-hidden="true"><span class="kole-m-switch__knob"></span></span>
<span class="kole-m-switch__text">small(40×22)</span>
</button>
</div>
</section>labelPlacement=left:整行右对齐的值区风格,文字在开关左侧。
查看代码(演示页原文 · 9 行)
<section class="demo-block" data-demo="left">
<p class="demo-label">文字在左(labelPlacement=left:整行右对齐的值区风格)</p>
<div class="demo-box" data-assert="switch-left">
<button class="kole-m-switch kole-m-switch--left is-on" type="button" role="switch" aria-checked="true">
<span class="kole-m-switch__track" aria-hidden="true"><span class="kole-m-switch__knob"></span></span>
<span class="kole-m-switch__text">自动同步(成功后写入云端)</span>
</button>
</div>
</section>02 组件状态
两者不能只靠颜色区分:滑块位置 + aria-checked 双通道。
查看代码(演示页原文 · 14 行)
<section class="demo-block" data-demo="states">
<p class="demo-label">关态与开态(两者不能只靠颜色区分:滑块位置 + aria-checked 双通道)</p>
<div class="demo-box" data-assert="switch-states">
<button class="kole-m-switch" type="button" role="switch" aria-checked="false" data-switch="off">
<span class="kole-m-switch__track" aria-hidden="true"><span class="kole-m-switch__knob"></span></span>
<span class="kole-m-switch__text">关态(默认)</span>
</button>
<button class="kole-m-switch is-on" type="button" role="switch" aria-checked="true" id="sw-on" data-switch="on"
data-behavior="click-sets-attr:#sw-on|aria-checked|false">
<span class="kole-m-switch__track" aria-hidden="true"><span class="kole-m-switch__knob"></span></span>
<span class="kole-m-switch__text">开态(滑块右移 + 品牌底色)</span>
</button>
</div>
</section>置灰且不响应;读屏会播报不可用。
查看代码(演示页原文 · 10 行)
<section class="demo-block" data-demo="disabled">
<p class="demo-label">禁用(置灰且不响应;读屏会播报不可用)</p>
<div class="demo-box" data-assert="switch-disabled">
<button class="kole-m-switch kole-m-switch--small is-disabled" type="button" role="switch" aria-checked="false"
aria-disabled="true" disabled>
<span class="kole-m-switch__track" aria-hidden="true"><span class="kole-m-switch__knob"></span></span>
<span class="kole-m-switch__text">内测功能(需管理员开启)</span>
</button>
</div>
</section>API
props / events / slots 为 6 端实现的公共接口(说明文字取自规格对应小节)。字段名与各端源码逐名核对:node tools/verify-mobile-docs.mjs
Props
| 名称 | 类型 | 默认值 | 说明 | 必传 |
|---|---|---|---|---|
checked | boolean | false | 受控开关值;与 aria-checked 同步写(规格 §29.5) | N |
size | 'default' | 'small' | 'default' | 变体 size:default 轨道 48×28,small 轨道 40×22(规格 §29.3) | N |
labelPlacement | 'right' | 'left' | 'right' | 变体 labelPlacement:文字在开关右侧还是左侧(规格 §29.3) | N |
disabled | boolean | false | 状态 disabled:置灰且不可聚焦(规格 §29.4) | N |
label | string | '' | 文字标签;同时作为无障碍名称落到 aria-label(规格 §29.6) | N |
「必传」按严格定义:实现里**没有默认值**时才为 Y(本门禁逐条核对 props 与各端源码的默认值,防止契约与实现脱节)。
事件
| 名称 | 参数 | 说明 |
|---|---|---|
change | (checked) | 切换时触发,回传切换后的目标值(规格 §29.5) |
插槽
| 名称 | 说明 |
|---|---|
default | 文字标签内容,优先于 label 属性(规格 §29.2 text) |
CSS 变量
组件级变量(在组件样式表里定义)。业务侧可在自己的作用域内覆盖,不必改组件源码。
| 名称 | 默认值 | 说明 |
|---|---|---|
--kole-m-switch-track-w | 48px | 轨道宽 |
--kole-m-switch-track-h | 28px | 轨道高 |
--kole-m-switch-knob | 24px | 滑块直径(轨道高 − 2×2px 内边距) |
--kole-m-switch-duration | 150ms | 滑块位移与底色过渡时长 |
--kole-m-switch-track-w | 40px | 组件内部默认值,可在业务侧覆盖 |
--kole-m-switch-track-h | 22px | 组件内部默认值,可在业务侧覆盖 |
--kole-m-switch-knob | 18px | 组件内部默认值,可在业务侧覆盖 |
何时使用
- 即时启停一项配置或业务状态(启用通知、公开数据、自动同步)
- 移动端开关本体视觉只有 48×28,但整行(开关 + 文字)都是可点热区,行高不小于 44px
- 关态与开态不能只靠颜色区分,必须同时看到滑块位移
- 点击切换只需要一次触摸,不要求拖动滑块(拖动是桌面习惯,触屏误触率高)
- 切换后立即触发 change 事件,不做二次确认(需要确认的场景由宿主先弹对话框)
交互与触控
- 整行(开关 + 文字)都是热区,行高不小于 44px;开关本体不可单独缩到 44px 以下
- 点击切换只需要一次触摸,不要求拖动滑块(拖动是桌面习惯,触屏误触率高)
- 切换动效是滑块位移 150ms 过渡;减少动态偏好下瞬时切换
- 关态与开态不能只靠颜色区分:滑块位置 + aria-checked 双通道
- 切换后立即触发 change 事件,不做二次确认(需要确认的场景由宿主先弹对话框)
无障碍
- 用 role="switch" + aria-checked="true|false",而不是 role="checkbox"(读屏会播报「开关」)
- 承载控件是原生 button,键盘可聚焦、空格/回车可切换,并有可见焦点环
- 文字标签在控件内部,读屏播报的名称就是标签本身;无标签时用 label 属性补 aria-label
- 禁用态用原生 disabled,读屏会播报不可用
相似组件
从「该用哪一个」的角度区分;PC 端的对应实现见 PC 文档站。
| 组件 | 何时用它而不是本组件 |
|---|---|
| 单元格Cell | 开关常作为单元格的右侧内容(Cell 负责行结构与分隔线,开关只管切换) |
| 按钮Button | 需要用户确认后一次性提交的用按钮;状态需要即时生效的用开关 |
| 对话框Dialog | 切换前需要用户确认(如扣费项)时先弹对话框;开关本身不做二次确认 |
规格未定 / 禁止发明
| 类别 | 条目 |
|---|---|
| 禁止发明 | 二次确认弹窗与「切换失败回滚」的业务流程 |
| 禁止发明 | 三态开关(关 / 开 / 待定)的视觉表达 |
| 禁止发明 | 与表单一起提交时的隐藏字段(由宿主添加) |
| 规格未定 | 开关本体是否允许小于 48×28(紧凑表单的下限) |
| 规格未定 | 文案与开关的间距是否跟随字号 |
| 规格未定 | 加载态(切换请求进行中)如何表达 |
结构(anatomy)
| 字段 | 说明 |
|---|---|
switch | 根元素,一行里放进「轨道 + 文字标签」,整行可点 |
track | 轨道,承载背景色与滑块位移的边界 |
knob | 滑块,关态靠左、开态靠右(位移是开/关的主要视觉信号) |
text | 可选文字标签,说明这项开关控制什么 |
control | 可点的整行控件(原生 button + role="switch"),承接键盘与读屏 |
变体维度与类名映射
类名映射由构建脚本从契约 variantClasses 生成,并被 verify:mobile-docs 逐条对照组件 CSS 校验(类/变量必须真实存在)。
| 维度 | 取值 | 对应类名 / 变量 |
|---|---|---|
size | default / small | default (由数据驱动,无专属类) small .kole-m-switch--small |
labelPlacement | right / left | right (由数据驱动,无专属类) left .kole-m-switch--left |
代表变体
| 变体 | 标签 |
|---|---|
size=default · labelPlacement=right | 默认(轨道 48×28,文字在右) |
size=small · labelPlacement=right | 紧凑(轨道 40×22,紧凑表单用) |
size=default · labelPlacement=left | 文字在左(整行右对齐的值区风格) |
用到的令牌
构建时从本组件样式表扫描得出。蓝色为移动端自有令牌,绿色为继承的 PC 令牌(改一处两端生效)。
6 端源码
同一组件的六份实现(生产环境的类名与结构一致,差异只在技术栈写法与单位)。点开查看,右侧可复制。
frameworks-mobile/Switch.css · 纯样式(CSS) · 98 行
/* Kole UI Mobile · Switch 样式 — 对齐移动端规格 §29
开关:轨道 + 滑块,开态用「滑块位移 + 品牌底色」双通道表达(不靠颜色单独表意);
整行(开关 + 文字)是热区,行高不小于 44px —— 开关本体视觉只有 48×28,单点本体手指点不中;
控件是原生 button + role="switch" + aria-checked(读屏播报「开关」,不是「复选框」)。 */
.kole-m-switch {
/* 组件级变量:业务侧可在容器上覆盖 */
--kole-m-switch-track-w: 48px; /* 轨道宽 */
--kole-m-switch-track-h: 28px; /* 轨道高 */
--kole-m-switch-knob: 24px; /* 滑块直径(轨道高 − 2×2px 内边距) */
--kole-m-switch-duration: 150ms; /* 滑块位移与底色过渡时长 */
box-sizing: border-box;
display: flex;
align-items: center;
gap: var(--kole-space-12);
width: 100%;
min-height: var(--kole-m-touch-target);
padding: var(--kole-space-8) var(--kole-m-gutter);
border: 0;
background: var(--kole-color-card-bg);
color: var(--kole-color-text-body);
font-family: inherit;
font-size: var(--kole-m-font-size-body);
line-height: 1.4;
text-align: start;
cursor: pointer;
touch-action: manipulation;
}
/* 变体 labelPlacement=left:文字在左、开关在右(整行右对齐的值区风格) */
.kole-m-switch--left { flex-direction: row-reverse; }
/* 变体 labelPlacement=left 时文字占满,开关贴右 */
.kole-m-switch--left .kole-m-switch__text { text-align: end; }
.kole-m-switch__track {
position: relative;
flex: 0 0 auto;
display: inline-block;
box-sizing: border-box;
width: var(--kole-m-switch-track-w);
height: var(--kole-m-switch-track-h);
border-radius: 999px;
/* 关态轨道用「未选中图标」色(3.23:1):开关状态承载信息,需满足 WCAG 1.4.11 的 3:1 */
background: var(--kole-color-icon-inactive);
transition: background var(--kole-m-switch-duration) var(--kole-ease-standard);
}
.kole-m-switch__knob {
position: absolute;
top: 2px;
left: 2px;
box-sizing: border-box;
width: var(--kole-m-switch-knob);
height: var(--kole-m-switch-knob);
border-radius: 50%;
background: var(--kole-color-card-bg);
box-shadow: var(--kole-shadow-low);
transition: transform var(--kole-m-switch-duration) var(--kole-ease-standard);
}
.kole-m-switch__text { flex: 1 1 auto; min-width: 0; }
/* 状态 on:滑块右移 + 轨道变品牌色(与 aria-checked="true" 同步写) */
.kole-m-switch.is-on .kole-m-switch__track { background: var(--kole-color-brand); }
.kole-m-switch.is-on .kole-m-switch__knob {
transform: translateX(calc(var(--kole-m-switch-track-w) - var(--kole-m-switch-knob) - 4px));
}
/* 变体 size=small:紧凑表单用(轨道 40×22) */
.kole-m-switch--small {
--kole-m-switch-track-w: 40px;
--kole-m-switch-track-h: 22px;
--kole-m-switch-knob: 18px;
}
/* 状态 disabled:置灰且不响应 */
.kole-m-switch.is-disabled {
color: var(--kole-color-text-disabled);
cursor: not-allowed;
}
.kole-m-switch.is-disabled:active { background: var(--kole-color-card-bg); }
.kole-m-switch:active:not(.is-disabled) { background: var(--kole-color-table-header-bg); }
.kole-m-switch:focus-visible {
outline: 2px solid var(--kole-color-focus-ring);
outline-offset: -2px;
}
/* 减少动态偏好:滑块瞬时到位,不做过场动画 */
@media (prefers-reduced-motion: reduce) {
.kole-m-switch__track,
.kole-m-switch__knob { transition: none; }
}
frameworks-mobile/Switch.html · H5 原生(无框架) · 123 行
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>Kole UI Mobile · Switch(H5)</title>
<link rel="stylesheet" href="../.design_library/kole-ui-mobile/colors_and_type.css">
<link rel="stylesheet" href="Switch.css">
<style>
body { margin: 0; background: var(--kole-color-page-bg); font-family: var(--kole-font-family); color: var(--kole-color-text-body); }
.demo { max-width: 375px; margin: 0 auto; padding: var(--kole-m-gutter) 0; }
.demo-label { margin: 0; padding: var(--kole-space-12) var(--kole-m-gutter) var(--kole-space-8);
font-size: var(--kole-m-font-size-label); color: var(--kole-color-text-secondary); }
.demo-box { background: var(--kole-color-card-bg); border-block: 1px solid var(--kole-color-border); }
.demo-block[hidden] { display: none; }
</style>
</head>
<body>
<div class="demo">
<section class="demo-block" data-demo="basic">
<p class="demo-label">基础用法(整行可点:开关本体只有 48×28,单点本体手指点不中)</p>
<div class="demo-box">
<button class="kole-m-switch is-on" type="button" role="switch" aria-checked="true" id="sw-product"
data-assert="switch-basic" data-behavior="click-toggles-class:#sw-product|is-on">
<span class="kole-m-switch__track" aria-hidden="true"><span class="kole-m-switch__knob"></span></span>
<span class="kole-m-switch__text">商品公开可见</span>
</button>
</div>
</section>
<section class="demo-block" data-demo="states">
<p class="demo-label">关态与开态(两者不能只靠颜色区分:滑块位置 + aria-checked 双通道)</p>
<div class="demo-box" data-assert="switch-states">
<button class="kole-m-switch" type="button" role="switch" aria-checked="false" data-switch="off">
<span class="kole-m-switch__track" aria-hidden="true"><span class="kole-m-switch__knob"></span></span>
<span class="kole-m-switch__text">关态(默认)</span>
</button>
<button class="kole-m-switch is-on" type="button" role="switch" aria-checked="true" id="sw-on" data-switch="on"
data-behavior="click-sets-attr:#sw-on|aria-checked|false">
<span class="kole-m-switch__track" aria-hidden="true"><span class="kole-m-switch__knob"></span></span>
<span class="kole-m-switch__text">开态(滑块右移 + 品牌底色)</span>
</button>
</div>
</section>
<section class="demo-block" data-demo="size">
<p class="demo-label">尺寸两档(size=default 48×28 / size=small 40×22,紧凑表单用)</p>
<div class="demo-box" data-assert="switch-size">
<button class="kole-m-switch is-on" type="button" role="switch" aria-checked="true">
<span class="kole-m-switch__track" aria-hidden="true"><span class="kole-m-switch__knob"></span></span>
<span class="kole-m-switch__text">default(48×28)</span>
</button>
<button class="kole-m-switch kole-m-switch--small is-on" type="button" role="switch" aria-checked="true">
<span class="kole-m-switch__track" aria-hidden="true"><span class="kole-m-switch__knob"></span></span>
<span class="kole-m-switch__text">small(40×22)</span>
</button>
</div>
</section>
<section class="demo-block" data-demo="left">
<p class="demo-label">文字在左(labelPlacement=left:整行右对齐的值区风格)</p>
<div class="demo-box" data-assert="switch-left">
<button class="kole-m-switch kole-m-switch--left is-on" type="button" role="switch" aria-checked="true">
<span class="kole-m-switch__track" aria-hidden="true"><span class="kole-m-switch__knob"></span></span>
<span class="kole-m-switch__text">自动同步(成功后写入云端)</span>
</button>
</div>
</section>
<section class="demo-block" data-demo="disabled">
<p class="demo-label">禁用(置灰且不响应;读屏会播报不可用)</p>
<div class="demo-box" data-assert="switch-disabled">
<button class="kole-m-switch kole-m-switch--small is-disabled" type="button" role="switch" aria-checked="false"
aria-disabled="true" disabled>
<span class="kole-m-switch__track" aria-hidden="true"><span class="kole-m-switch__knob"></span></span>
<span class="kole-m-switch__text">内测功能(需管理员开启)</span>
</button>
</div>
</section>
</div>
<script>
/* 演示页脚本:真实的切换。
- 点击:切 is-on + aria-checked(两者必须同步写,只切类会让读屏读到旧状态)
- 禁用项不响应(原生 disabled 已拦住 click,这里再兜一次)
真实业务里这份状态由宿主管理(受控 checked + change 事件),此处是最小可运行实现。 */
(function () {
function sync(btn) {
var on = btn.classList.contains('is-on');
btn.setAttribute('aria-checked', on ? 'true' : 'false');
}
Array.prototype.forEach.call(document.querySelectorAll('.kole-m-switch'), function (btn) {
if (btn.disabled || btn.classList.contains('is-disabled')) return;
btn.addEventListener('click', function () {
btn.classList.toggle('is-on');
sync(btn);
});
});
})();
</script>
<script>
/* ?demo=<id> → 只显示该演示块(文档站按块预览用;无参数时全部显示,测试与回归走无参数路径) */
(function () {
var id = new URLSearchParams(location.search).get('demo');
if (!id) return;
var blocks = Array.prototype.slice.call(document.querySelectorAll('.demo-block'));
var hit = false;
blocks.forEach(function (b) {
var on = b.getAttribute('data-demo') === id;
if (on) hit = true;
b.hidden = !on;
});
if (!hit) { blocks.forEach(function (b) { b.hidden = false; }); return; }
document.body.classList.add('demo-single');
blocks.forEach(function (b) {
var label = b.querySelector('.demo-label');
if (label && !b.hidden) label.hidden = true;
});
})();
</script>
</body>
</html>
frameworks-mobile/Switch.jsx · React · 47 行
import React from 'react';
import './Switch.css';
/* 开关(移动端)— 规格 §29
控件是原生 button + role="switch" + aria-checked(读屏播报「开关」,不是「复选框」)。
整行(轨道 + 文字)都是热区,行高不小于 44px —— 开关本体视觉只有 48×28。
开态用「滑块位移 + 品牌底色」双通道表达,不靠颜色单独表意。
本端只回传切换意图(onChange),是否真的生效由宿主决定(规格 §29.5)。 */
export default function Switch({
checked = false,
size = 'default',
labelPlacement = 'right',
disabled = false,
label = '',
onChange,
children = null,
}) {
const cls =
'kole-m-switch' +
(size === 'small' ? ' kole-m-switch--small' : '') +
(labelPlacement === 'left' ? ' kole-m-switch--left' : '') +
(checked ? ' is-on' : '') +
(disabled ? ' is-disabled' : '');
return (
<button
className={cls}
type="button"
role="switch"
aria-checked={checked ? 'true' : 'false'}
aria-label={label || undefined}
aria-disabled={disabled ? 'true' : undefined}
disabled={disabled}
onClick={() => {
if (disabled) return;
if (onChange) onChange(!checked);
}}
>
<span className="kole-m-switch__track" aria-hidden="true">
<span className="kole-m-switch__knob" />
</span>
{children || label ? <span className="kole-m-switch__text">{children || label}</span> : null}
</button>
);
}
frameworks-mobile/Switch.vue2.vue · Vue 2 · 56 行
<template>
<button
class="kole-m-switch"
:class="switchClass"
type="button"
role="switch"
:aria-checked="checked ? 'true' : 'false'"
:aria-label="label || null"
:aria-disabled="disabled ? 'true' : null"
:disabled="disabled"
@click="onClick"
>
<span class="kole-m-switch__track" aria-hidden="true">
<span class="kole-m-switch__knob"></span>
</span>
<span v-if="$slots.default || label" class="kole-m-switch__text"><slot>{{ label }}</slot></span>
</button>
</template>
<script>
/* 开关(移动端)— 规格 §29
控件是原生 button + role="switch" + aria-checked(读屏播报「开关」,不是「复选框」)。
整行(轨道 + 文字)都是热区,行高不小于 44px —— 开关本体视觉只有 48×28。
开态用「滑块位移 + 品牌底色」双通道表达,不靠颜色单独表意。
本端只回传切换意图(change),是否真的生效由宿主决定(规格 §29.5)。 */
export default {
name: 'KoleMSwitch',
props: {
checked: { type: Boolean, default: false },
size: { type: String, default: 'default' },
labelPlacement: { type: String, default: 'right' },
disabled: { type: Boolean, default: false },
label: { type: String, default: '' }
},
computed: {
switchClass: function () {
return [
this.size === 'small' ? 'kole-m-switch--small' : '',
this.labelPlacement === 'left' ? 'kole-m-switch--left' : '',
this.checked ? 'is-on' : '',
this.disabled ? 'is-disabled' : ''
].filter(Boolean);
}
},
methods: {
onClick: function () {
if (this.disabled) return;
this.$emit('change', !this.checked);
}
}
};
</script>
<style src="./Switch.css"></style>
frameworks-mobile/Switch.vue3.vue · Vue 3 · 51 行
<template>
<button
class="kole-m-switch"
:class="switchClass"
type="button"
role="switch"
:aria-checked="checked ? 'true' : 'false'"
:aria-label="label || null"
:aria-disabled="disabled ? 'true' : null"
:disabled="disabled"
@click="onClick"
>
<span class="kole-m-switch__track" aria-hidden="true">
<span class="kole-m-switch__knob"></span>
</span>
<span v-if="$slots.default || label" class="kole-m-switch__text"><slot>{{ label }}</slot></span>
</button>
</template>
<script setup>
/* 开关(移动端)— 规格 §29
控件是原生 button + role="switch" + aria-checked(读屏播报「开关」,不是「复选框」)。
整行(轨道 + 文字)都是热区,行高不小于 44px —— 开关本体视觉只有 48×28。
开态用「滑块位移 + 品牌底色」双通道表达,不靠颜色单独表意。
本端只回传切换意图(change),是否真的生效由宿主决定(规格 §29.5)。 */
import { computed } from 'vue';
const props = defineProps({
checked: { type: Boolean, default: false },
size: { type: String, default: 'default' },
labelPlacement: { type: String, default: 'right' },
disabled: { type: Boolean, default: false },
label: { type: String, default: '' }
});
const emit = defineEmits(['change']);
const switchClass = computed(() => [
props.size === 'small' ? 'kole-m-switch--small' : '',
props.labelPlacement === 'left' ? 'kole-m-switch--left' : '',
props.checked ? 'is-on' : '',
props.disabled ? 'is-disabled' : ''
].filter(Boolean));
function onClick() {
if (props.disabled) return;
emit('change', !props.checked);
}
</script>
<style src="./Switch.css"></style>
frameworks-mobile/Switch.uniapp.vue · uni-app(跨端:小程序 / App / H5) · 121 行
<template>
<view
class="kole-m-switch"
:class="switchClass"
:role="disabled ? '' : 'switch'"
:aria-checked="checked ? 'true' : 'false'"
:aria-disabled="disabled ? 'true' : 'false'"
:aria-label="label || ''"
@tap="onTap"
>
<view class="kole-m-switch__track">
<view class="kole-m-switch__knob"></view>
</view>
<text v-if="label" class="kole-m-switch__text">{{ label }}</text>
<slot></slot>
</view>
</template>
<script setup>
/* uni-app 端 · 开关(移动端)— 规格 §29
跨端差异:
① 小程序端不用 uni 的 <switch> 基础组件:它无法承载「文字在左 / 紧凑尺寸」这类形态,
也带不进 kole-m- 的样式与令牌,故用 view 自绘 + role="switch" + aria-checked;
② 点击用 @tap(触屏),不依赖 pointer/mouse;
③ 滑块位移与底色过渡由 CSS transition 承担,代码里不做动画。
尺寸用 rpx:88rpx = 375pt 下的 44px 触控最小边长,故整行最小高 88rpx,
轨道 96rpx×56rpx(= 48×28),滑块 48rpx(= 24)。 */
import { computed } from 'vue';
const props = defineProps({
checked: { type: Boolean, default: false },
size: { type: String, default: 'default' },
labelPlacement: { type: String, default: 'right' },
disabled: { type: Boolean, default: false },
label: { type: String, default: '' }
});
const emit = defineEmits(['change']);
const switchClass = computed(() => [
props.size === 'small' ? 'kole-m-switch--small' : '',
props.labelPlacement === 'left' ? 'kole-m-switch--left' : '',
props.checked ? 'is-on' : '',
props.disabled ? 'is-disabled' : ''
].filter(Boolean));
function onTap() {
if (props.disabled) return;
emit('change', !props.checked);
}
</script>
<style>
.kole-m-switch {
--kole-m-switch-track-w: 96rpx; /* 轨道宽(96rpx = 48px) */
--kole-m-switch-track-h: 56rpx; /* 轨道高(56rpx = 28px) */
--kole-m-switch-knob: 48rpx; /* 滑块直径(48rpx = 24px) */
--kole-m-touch-target: 88rpx;
--kole-m-font-size-body: 32rpx;
--kole-m-gutter: 32rpx;
box-sizing: border-box;
display: flex;
align-items: center;
width: 100%;
min-height: var(--kole-m-touch-target);
padding: 16rpx var(--kole-m-gutter);
background-color: var(--kole-color-card-bg);
color: var(--kole-color-text-body);
font-size: var(--kole-m-font-size-body);
}
.kole-m-switch--left { flex-direction: row-reverse; }
.kole-m-switch--left .kole-m-switch__text { text-align: right; }
.kole-m-switch__track {
position: relative;
flex-shrink: 0;
width: var(--kole-m-switch-track-w);
height: var(--kole-m-switch-track-h);
border-radius: 999rpx;
background-color: var(--kole-color-icon-inactive);
transition: background-color 150ms ease;
}
.kole-m-switch__knob {
position: absolute;
top: 4rpx;
left: 4rpx;
width: var(--kole-m-switch-knob);
height: var(--kole-m-switch-knob);
border-radius: 50%;
background-color: var(--kole-color-card-bg);
transition: transform 150ms ease;
}
/* 状态 on:滑块右移 + 轨道变品牌色 */
.kole-m-switch.is-on .kole-m-switch__track { background-color: var(--kole-color-brand); }
.kole-m-switch.is-on .kole-m-switch__knob {
transform: translateX(calc(var(--kole-m-switch-track-w) - var(--kole-m-switch-knob) - 8rpx));
}
/* 变体 size=small:通道 80rpx×44rpx(= 40×22),滑块 36rpx(= 18) */
.kole-m-switch--small {
--kole-m-switch-track-w: 80rpx;
--kole-m-switch-track-h: 44rpx;
--kole-m-switch-knob: 36rpx;
}
.kole-m-switch__text {
flex: 1;
padding-left: 24rpx;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.kole-m-switch--left .kole-m-switch__text { padding-left: 0; padding-right: 24rpx; }
.kole-m-switch.is-disabled { color: var(--kole-color-text-disabled); }
</style>
测试与回归
断言在真实的 375×640 设备帧里跑(引擎与 PC 侧共用 tests/_runtime.js,触控行为动词来自移动端 tests/mobile/_behaviors.js)。
断言 17 条 · 全部通过 报告 2026-09-22 23:01:05
node site/dev-server.js &
REG_BASE=http://127.0.0.1:3311 node tools/run-mobile-regression.mjs # 全量 5 个组件
npm run verify:mobile-docs # 本页内容完整性 + API 与源码一致性
设计契约
components/mobile-switch.json(点击展开原始 JSON)
{
"schemaVersion": 1,
"sourceKind": "authored-spec",
"provenance": "authored-in-repo",
"specFile": "spec/移动端规格.md",
"specSection": "29 · 开关 Switch",
"confidence": "high",
"slug": "mobile-switch",
"name": "开关 Switch",
"semanticTypeCandidates": [
"switch",
"toggle",
"boolean-input"
],
"variantDimensions": [
{
"name": "size",
"values": [
"default",
"small"
]
},
{
"name": "labelPlacement",
"values": [
"right",
"left"
]
}
],
"representativeVariants": [
{
"size": "default",
"labelPlacement": "right",
"label": "默认(轨道 48×28,文字在右)"
},
{
"size": "small",
"labelPlacement": "right",
"label": "紧凑(轨道 40×22,紧凑表单用)"
},
{
"size": "default",
"labelPlacement": "left",
"label": "文字在左(整行右对齐的值区风格)"
}
],
"anatomy": {
"switch": "根元素,一行里放进「轨道 + 文字标签」,整行可点",
"track": "轨道,承载背景色与滑块位移的边界",
"knob": "滑块,关态靠左、开态靠右(位移是开/关的主要视觉信号)",
"text": "可选文字标签,说明这项开关控制什么",
"control": "可点的整行控件(原生 button + role=\"switch\"),承接键盘与读屏"
},
"structurePatterns": {
"size": "default(轨道 48×28)/ small(轨道 40×22,用于紧凑表单)",
"labelPlacement": "right(文字在开关右侧,默认)/ left(文字在左侧,值区右对齐时用)"
},
"usageHints": [
"即时启停一项配置或业务状态(启用通知、公开数据、自动同步)",
"移动端开关本体视觉只有 48×28,但整行(开关 + 文字)都是可点热区,行高不小于 44px",
"关态与开态不能只靠颜色区分,必须同时看到滑块位移",
"点击切换只需要一次触摸,不要求拖动滑块(拖动是桌面习惯,触屏误触率高)",
"切换后立即触发 change 事件,不做二次确认(需要确认的场景由宿主先弹对话框)"
],
"doNotInvent": [
"二次确认弹窗与「切换失败回滚」的业务流程",
"三态开关(关 / 开 / 待定)的视觉表达",
"与表单一起提交时的隐藏字段(由宿主添加)"
],
"unknowns": [
"开关本体是否允许小于 48×28(紧凑表单的下限)",
"文案与开关的间距是否跟随字号",
"加载态(切换请求进行中)如何表达"
],
"interaction": [
"整行(开关 + 文字)都是热区,行高不小于 44px;开关本体不可单独缩到 44px 以下",
"点击切换只需要一次触摸,不要求拖动滑块(拖动是桌面习惯,触屏误触率高)",
"切换动效是滑块位移 150ms 过渡;减少动态偏好下瞬时切换",
"关态与开态不能只靠颜色区分:滑块位置 + aria-checked 双通道",
"切换后立即触发 change 事件,不做二次确认(需要确认的场景由宿主先弹对话框)"
],
"accessibility": [
"用 role=\"switch\" + aria-checked=\"true|false\",而不是 role=\"checkbox\"(读屏会播报「开关」)",
"承载控件是原生 button,键盘可聚焦、空格/回车可切换,并有可见焦点环",
"文字标签在控件内部,读屏播报的名称就是标签本身;无标签时用 label 属性补 aria-label",
"禁用态用原生 disabled,读屏会播报不可用"
],
"api": {
"source": "implementation",
"note": "props / events / slots 为 6 端实现的公共接口(说明文字取自规格对应小节)。字段名与各端源码逐名核对:node tools/verify-mobile-docs.mjs",
"requiredNote": "「必传」按严格定义:实现里**没有默认值**时才为 Y(本门禁逐条核对 props 与各端源码的默认值,防止契约与实现脱节)。",
"props": [
{
"name": "checked",
"type": "boolean",
"default": "false",
"desc": "受控开关值;与 aria-checked 同步写(规格 §29.5)",
"required": false
},
{
"name": "size",
"type": "'default' | 'small'",
"default": "'default'",
"desc": "变体 size:default 轨道 48×28,small 轨道 40×22(规格 §29.3)",
"required": false
},
{
"name": "labelPlacement",
"type": "'right' | 'left'",
"default": "'right'",
"desc": "变体 labelPlacement:文字在开关右侧还是左侧(规格 §29.3)",
"required": false
},
{
"name": "disabled",
"type": "boolean",
"default": "false",
"desc": "状态 disabled:置灰且不可聚焦(规格 §29.4)",
"required": false
},
{
"name": "label",
"type": "string",
"default": "''",
"desc": "文字标签;同时作为无障碍名称落到 aria-label(规格 §29.6)",
"required": false
}
],
"events": [
{
"name": "change",
"params": "(checked)",
"desc": "切换时触发,回传切换后的目标值(规格 §29.5)"
}
],
"slots": [
{
"name": "default",
"desc": "文字标签内容,优先于 label 属性(规格 §29.2 text)"
}
]
},
"variantClasses": {
"size": {
"default": [],
"small": [
".kole-m-switch--small"
]
},
"labelPlacement": {
"right": [],
"left": [
".kole-m-switch--left"
]
}
},
"demos": [
{
"id": "basic",
"group": "01 组件类型",
"title": "基础用法",
"desc": "整行可点:开关本体只有 48×28,单点本体手指点不中。",
"variant": "size=default"
},
{
"id": "states",
"group": "02 组件状态",
"title": "关态与开态",
"desc": "两者不能只靠颜色区分:滑块位置 + aria-checked 双通道。",
"variant": "状态 off|on"
},
{
"id": "size",
"group": "01 组件类型",
"title": "尺寸两档",
"desc": "default 轨道 48×28;small 轨道 40×22 用于紧凑表单。",
"variant": "size=default|small"
},
{
"id": "left",
"group": "01 组件类型",
"title": "文字在左",
"desc": "labelPlacement=left:整行右对齐的值区风格,文字在开关左侧。",
"variant": "labelPlacement=left"
},
{
"id": "disabled",
"group": "02 组件状态",
"title": "禁用",
"desc": "置灰且不响应;读屏会播报不可用。",
"variant": "disabled=true"
}
],
"related": [
{
"slug": "cell",
"why": "开关常作为单元格的右侧内容(Cell 负责行结构与分隔线,开关只管切换)"
},
{
"slug": "mobile-button",
"why": "需要用户确认后一次性提交的用按钮;状态需要即时生效的用开关"
},
{
"slug": "mobile-dialog",
"why": "切换前需要用户确认(如扣费项)时先弹对话框;开关本身不做二次确认"
}
]
}