移动端导航多选框

多选框Checkbox

在一组选项里同时选中任意多项(兴趣标签、订阅范围、筛选维度),并支持「全选」

数据录入 规格 34 · 多选框 Checkbox 6 端实现 触摸优先

引入(H5 原生;其余 5 端见「快速开始」)
<!-- ① 令牌: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-checkbox.css">

<!-- ③ 结构照抄下方任一演示块(类名与 6 端实现一致) -->

演示

每个演示都是真实渲染:预览帧加载 frameworks-mobile/Checkbox.html?demo=<id>(只显示该演示块),代码是该演示块在演示页里的原文,可复制。全部演示同屏可看 演示页 ↗。

01 组件类型

复选框组orientation=vertical

多选:各项互不影响;点一项即切换该项的选中态。

查看代码(演示页原文 · 18 行)
frameworks-mobile/Checkbox.html · basic
<section class="demo-block" data-demo="basic">
  <p class="demo-label">复选框组(多选:各项互不影响;点一项即切换该项的选中态)</p>
  <div class="kole-m-checkbox-group" role="group" aria-label="兴趣标签" id="cb-basic" data-value="news,market" data-assert="checkbox-basic">
    <button class="kole-m-checkbox is-checked" type="button" role="checkbox" aria-checked="true" data-checkbox="news">
      <span class="kole-m-checkbox__icon" aria-hidden="true"></span>
      <span class="kole-m-checkbox__label">行业新闻</span>
    </button>
    <button class="kole-m-checkbox" type="button" role="checkbox" aria-checked="false" data-checkbox="report"
            data-behavior="click-sets-attr:#cb-basic|data-value|news,report,market">
      <span class="kole-m-checkbox__icon" aria-hidden="true"></span>
      <span class="kole-m-checkbox__label">数据报告</span>
    </button>
    <button class="kole-m-checkbox is-checked" type="button" role="checkbox" aria-checked="true" data-checkbox="market">
      <span class="kole-m-checkbox__icon" aria-hidden="true"></span>
      <span class="kole-m-checkbox__label">营销活动</span>
    </button>
  </div>
</section>
全选与半选selectAll=true

全选行不存储自己的状态:全选 / 半选 / 空都由下面各项推导。

查看代码(演示页原文 · 24 行)
frameworks-mobile/Checkbox.html · all
<section class="demo-block" data-demo="all">
  <p class="demo-label">全选与半选(全选行不存储自己的状态:全选 / 半选 / 空都由下面三项推导)</p>
  <div class="kole-m-checkbox-group" role="group" aria-label="消息通知" id="cb-all"
       data-value="system,order" data-assert="checkbox-all">
    <button class="kole-m-checkbox kole-m-checkbox--all is-indeterminate" type="button" role="checkbox"
            aria-checked="mixed" id="cb-all-toggle" data-checkbox="__all__"
            data-behavior="click-sets-attr:#cb-all|data-value|system,order,promo">
      <span class="kole-m-checkbox__icon" aria-hidden="true"></span>
      <span class="kole-m-checkbox__label">全选</span>
    </button>
    <button class="kole-m-checkbox is-checked" type="button" role="checkbox" aria-checked="true" data-checkbox="system">
      <span class="kole-m-checkbox__icon" aria-hidden="true"></span>
      <span class="kole-m-checkbox__label">系统消息</span>
    </button>
    <button class="kole-m-checkbox is-checked" type="button" role="checkbox" aria-checked="true" data-checkbox="order">
      <span class="kole-m-checkbox__icon" aria-hidden="true"></span>
      <span class="kole-m-checkbox__label">订单消息</span>
    </button>
    <button class="kole-m-checkbox" type="button" role="checkbox" aria-checked="false" data-checkbox="promo">
      <span class="kole-m-checkbox__icon" aria-hidden="true"></span>
      <span class="kole-m-checkbox__label">营销推送</span>
    </button>
  </div>
</section>
横向排列orientation=horizontal

orientation=horizontal:值少、文案短时用,不再画分隔线。

查看代码(演示页原文 · 21 行)
frameworks-mobile/Checkbox.html · horizontal
<section class="demo-block" data-demo="horizontal">
  <p class="demo-label">横向排列(orientation=horizontal:值少、文案短时用,不再画分隔线)</p>
  <div class="demo-pad">
    <div class="kole-m-checkbox-group kole-m-checkbox-group--horizontal" role="group" aria-label="筛选维度"
         id="cb-horizontal" data-value="brand" data-assert="checkbox-horizontal">
      <button class="kole-m-checkbox is-checked" type="button" role="checkbox" aria-checked="true" data-checkbox="brand">
        <span class="kole-m-checkbox__icon" aria-hidden="true"></span>
        <span class="kole-m-checkbox__label">品牌</span>
      </button>
      <button class="kole-m-checkbox" type="button" role="checkbox" aria-checked="false" data-checkbox="price"
              data-behavior="click-sets-attr:#cb-horizontal|data-value|brand,price">
        <span class="kole-m-checkbox__icon" aria-hidden="true"></span>
        <span class="kole-m-checkbox__label">价格</span>
      </button>
      <button class="kole-m-checkbox" type="button" role="checkbox" aria-checked="false" data-checkbox="stock">
        <span class="kole-m-checkbox__icon" aria-hidden="true"></span>
        <span class="kole-m-checkbox__label">仅看有货</span>
      </button>
    </div>
  </div>
</section>

02 组件状态

三种选中态状态 checked | indeterminate

未选中 / 选中 / 半选:勾号出现与消失是形状差异,不只靠颜色。

查看代码(演示页原文 · 20 行)
frameworks-mobile/Checkbox.html · states
<section class="demo-block" data-demo="states">
  <p class="demo-label">三种选中态(未选中 / 选中 / 半选,全选行的半选由组内各项推导)</p>
  <div class="demo-pad" data-assert="checkbox-states">
    <div class="kole-m-checkbox-group">
      <button class="kole-m-checkbox" type="button" role="checkbox" aria-checked="false">
        <span class="kole-m-checkbox__icon" aria-hidden="true"></span>
        <span class="kole-m-checkbox__label">未选中:空心方框</span>
      </button>
      <button class="kole-m-checkbox is-checked" type="button" role="checkbox" aria-checked="true">
        <span class="kole-m-checkbox__icon" aria-hidden="true"></span>
        <span class="kole-m-checkbox__label">选中:品牌底 + 反色勾号</span>
      </button>
      <button class="kole-m-checkbox is-indeterminate" type="button" role="checkbox" aria-checked="mixed" id="cb-mixed"
              data-behavior="click-toggles-class:#cb-mixed|is-indeterminate">
        <span class="kole-m-checkbox__icon" aria-hidden="true"></span>
        <span class="kole-m-checkbox__label">半选:横杠(组内部分选中时)</span>
      </button>
    </div>
  </div>
</section>
禁用项disabled=true

置灰且不响应:原生 disabled 让读屏播报不可用、键盘跳过。

查看代码(演示页原文 · 16 行)
frameworks-mobile/Checkbox.html · disabled
<section class="demo-block" data-demo="disabled">
  <p class="demo-label">禁用项(置灰且不响应:原生 disabled 让读屏播报不可用、键盘跳过)</p>
  <div class="kole-m-checkbox-group" role="group" aria-label="订阅范围" data-assert="checkbox-disabled">
    <button class="kole-m-checkbox is-checked" type="button" role="checkbox" aria-checked="true">
      <span class="kole-m-checkbox__icon" aria-hidden="true"></span>
      <span class="kole-m-checkbox__label">站内信</span>
    </button>
    <button class="kole-m-checkbox" type="button" role="checkbox" aria-checked="false" aria-disabled="true" disabled>
      <span class="kole-m-checkbox__icon" aria-hidden="true"></span>
      <span class="kole-m-checkbox__label">
        短信提醒
        <span class="kole-m-checkbox__desc">需先绑定手机号</span>
      </span>
    </button>
  </div>
</section>

API

props / events / slots 为 6 端实现的公共接口(说明文字取自规格对应小节)。字段名与各端源码逐名核对:node tools/verify-mobile-docs.mjs

Props

名称类型默认值说明必传
optionsArray[]选项数组,每项 { value, label, desc, disabled }(规格 §34.2)N
valueArray[]受控选中值数组,每项与 aria-checked 同步写(规格 §34.5)N
orientation'vertical' | 'horizontal''vertical'变体 orientation:纵向(画分隔线)还是横向排列(规格 §34.3)N
buttonbooleanfalse变体 button:胶囊标签式(无方框),用于筛选条(规格 §34.3)N
selectAllbooleanfalse变体 selectAll:组首是否显示全选行(规格 §34.3)N
disabledbooleanfalse状态 disabled:整组置灰且不可聚焦(规格 §34.4)N
labelstring'多选项'分组名称,落到 group 的 aria-label(规格 §34.6)N

「必传」按严格定义:实现里**没有默认值**时才为 Y(本门禁逐条核对 props 与各端源码的默认值,防止契约与实现脱节)。

事件

名称参数说明
change(value)切换任一项或全选时触发,回传切换后的完整值数组(规格 §34.5)

插槽

名称说明
default额外选项内容(宿主自己渲染的项,追在 options 之后,规格 §34.2 item)

CSS 变量

组件级变量(在组件样式表里定义)。业务侧可在自己的作用域内覆盖,不必改组件源码。

名称默认值说明
--kole-m-checkbox-size20px方框边长(视觉尺寸)
--kole-m-checkbox-gapvar(--kole-space-12)方框与文字的间距

何时使用

  • 在一组选项里同时选中任意多项(兴趣标签、订阅范围、筛选维度),并支持「全选」
  • 触屏上没有鼠标悬停预告状态,全选行在「部分选中」时必须显示出与「全选 / 全不选」都不同的第三态(横杠)
  • 整行(方框 + 文字)都是热区,行高不小于 44px
  • 点全选行:只要还有未选中项就全部选中,否则全部清空(不在「半选」上停留)
  • 全选的选中态是推导值,不单独存储 —— 单项变化后立即重算

交互与触控

  • 整行(方框 + 文字)都是热区,行高不小于 44px;横向组里每项自身也保持这个边长
  • 一次触摸即切换,各项互相独立;不限制同时选中的数量上限
  • 点全选行:只要还有未选中项就全部选中,否则全部清空(不在「半选」上停留)
  • 全选的选中态是**推导值**,不单独存储 —— 单项变化后立即重算,避免出现「全选已勾上但还有一项没选」
  • 切换动效是勾号 150ms 缩放;减少动态偏好下瞬时切换
  • 每次切换立即触发 change 事件,回传切换后的完整值数组

无障碍

  • 组用 role="group" + aria-label 说明分组名称
  • 每项是原生 button + role="checkbox" + aria-checked(读屏会播报「复选框,已选中/未选中」)
  • 全选行的半选态用 aria-checked="mixed"(checkbox 角色允许的第三个值)
  • 方框与勾号是纯装饰,对读屏隐藏(aria-hidden),语义全靠 role="checkbox"
  • 禁用项用原生 disabled 并补 aria-disabled="true"

从「该用哪一个」的角度区分;PC 端的对应实现见 PC 文档站。

组件何时用它而不是本组件
单选框Radio选项之间互斥(只能选一项)时用 Radio;可以同时成立多项时才用 Checkbox
开关Switch单个开关类配置用 Switch(即时生效、二值);需要提交一组值时才用 Checkbox 组
表单Form复选框组作为表单里的一个字段时,标签、必填星号与错误提示由 Form 的 item 提供

规格未定 / 禁止发明

类别条目
禁止发明「最多选 N 项」的数量上限与超出提示(业务规则在宿主)
禁止发明分组嵌套(一组里再分组)的层级表达
禁止发明与表单一起提交时的隐藏字段(由宿主添加)
禁止发明选项内容的异步加载与「已选 N 项」的汇总条
规格未定全选行是否显示「已选 2/5」这类计数
规格未定半选态在非全选行上的用例(如父级节点)
规格未定胶囊按钮式是否也需要禁用态的视觉层(当前复用同一条置灰规则)

结构(anatomy)

字段说明
group根元素,role="group" + aria-label 说明这组在选什么
item单个选项,整行都是热区(原生 button + role="checkbox")
icon方框,未选中是空心框、选中是品牌底 + 反色勾号、半选是品牌底 + 横杠
label选项文字,占满剩余宽度
desc可选说明行,跟在文字下方(如「需先绑定手机号」)
all全选行,不存储自己的状态,由组内各项推导

变体维度与类名映射

类名映射由构建脚本从契约 variantClasses 生成,并被 verify:mobile-docs 逐条对照组件 CSS 校验(类/变量必须真实存在)。

维度取值对应类名 / 变量
orientationvertical / horizontal
vertical (由数据驱动,无专属类)
horizontal .kole-m-checkbox-group--horizontal
buttonfalse / true
false (由数据驱动,无专属类)
true .kole-m-checkbox--button
selectAllfalse / true
false (由数据驱动,无专属类)
true .kole-m-checkbox--all

代表变体

变体标签
orientation=vertical · button=false · selectAll=false默认(方框 + 文字,纵向排列并画分隔线)
orientation=vertical · button=false · selectAll=true带全选行(组首一行「全选」,三态由组内推导)
orientation=horizontal · button=true · selectAll=false胶囊标签式横向(无方框,筛选条用)

用到的令牌

构建时从本组件样式表扫描得出。蓝色为移动端自有令牌,绿色为继承的 PC 令牌(改一处两端生效)。

--kole-m-font-size-body --kole-m-font-size-label --kole-m-gutter --kole-m-touch-target --kole-color-border --kole-color-brand --kole-color-brand-bg --kole-color-card-bg --kole-color-disabled-bg --kole-color-focus-ring --kole-color-table-header-bg --kole-color-text-body --kole-color-text-disabled --kole-color-text-inverse --kole-color-text-placeholder --kole-color-text-secondary --kole-color-text-title --kole-ease-standard --kole-radius-small --kole-space-12 --kole-space-16 --kole-space-8 --kole-m-checkbox-gap --kole-m-checkbox-size

6 端源码

同一组件的六份实现(生产环境的类名与结构一致,差异只在技术栈写法与单位)。点开查看,右侧可复制。

frameworks-mobile/Checkbox.css · 纯样式(CSS) · 175 行
frameworks-mobile/Checkbox.css
/* Kole UI Mobile · Checkbox 样式 — 对齐移动端规格 §34
   多选框:一组里可以同时选中多项,并带「全选」形态(全选行的选中态由组内各项推导)。
   触屏上没有鼠标悬停,选中态必须同时有形状差异(勾号出现/消失)与 aria-checked,
   不能只靠颜色;整行(方框 + 文字)都是热区,行高不小于 44px。 */

.kole-m-checkbox-group {
  /* 组件级变量:业务侧可在容器上覆盖 */
  --kole-m-checkbox-size: 20px;              /* 方框边长(视觉尺寸) */
  --kole-m-checkbox-gap: var(--kole-space-12); /* 方框与文字的间距 */
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  width: 100%;
  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;
}

/* 变体 orientation=horizontal:横向排列(值少、文案短时用) */
.kole-m-checkbox-group--horizontal {
  flex-direction: row;
  flex-wrap: wrap;
  align-items: center;
}

/* 组内相邻项的分隔线:只在纵向排列时画 */
.kole-m-checkbox-group:not(.kole-m-checkbox-group--horizontal) .kole-m-checkbox + .kole-m-checkbox {
  border-top: 1px solid var(--kole-color-border);
}

.kole-m-checkbox {
  box-sizing: border-box;
  display: flex;
  align-items: center;
  gap: var(--kole-m-checkbox-gap);
  width: 100%;
  min-height: var(--kole-m-touch-target);
  padding: var(--kole-space-12) var(--kole-m-gutter);
  margin: 0;
  border: 0;
  background: var(--kole-color-card-bg);
  color: var(--kole-color-text-body);
  font-family: inherit;
  font-size: inherit;
  line-height: 1.4;
  text-align: start;
  cursor: pointer;
  touch-action: manipulation;
}

.kole-m-checkbox-group--horizontal .kole-m-checkbox {
  width: auto;
  padding-inline: var(--kole-space-12);
}

.kole-m-checkbox:active:not(:disabled) { background: var(--kole-color-table-header-bg); }

/* 方框:未选中是空心方框,选中是品牌色底 + 反色勾号(形状差异,不只靠颜色) */
.kole-m-checkbox__icon {
  position: relative;
  flex: 0 0 auto;
  box-sizing: border-box;
  width: var(--kole-m-checkbox-size);
  height: var(--kole-m-checkbox-size);
  border: 1px solid var(--kole-color-text-placeholder);
  border-radius: var(--kole-radius-small);
  background: var(--kole-color-card-bg);
  transition: border-color 150ms var(--kole-ease-standard), background-color 150ms var(--kole-ease-standard);
}

/* 勾号用两条边框拼(不引入图标字体):未选中时缩放为 0 */
.kole-m-checkbox__icon::after {
  content: '';
  position: absolute;
  top: 3px;
  left: 6px;
  width: 5px;
  height: 9px;
  border: solid var(--kole-color-text-inverse);
  border-width: 0 2px 2px 0;
  transform: rotate(45deg) scale(0);
  transition: transform 150ms var(--kole-ease-standard);
}

.kole-m-checkbox.is-checked { color: var(--kole-color-text-title); }

.kole-m-checkbox.is-checked .kole-m-checkbox__icon {
  border-color: var(--kole-color-brand);
  background: var(--kole-color-brand);
}

.kole-m-checkbox.is-checked .kole-m-checkbox__icon::after { transform: rotate(45deg) scale(1); }

/* 状态 indeterminate(半选,全选行的常见态):品牌底 + 一条横杠 */
.kole-m-checkbox.is-indeterminate .kole-m-checkbox__icon {
  border-color: var(--kole-color-brand);
  background: var(--kole-color-brand);
}

.kole-m-checkbox.is-indeterminate .kole-m-checkbox__icon::after {
  top: 8px;
  left: 3px;
  width: 10px;
  height: 0;
  border-width: 0 0 2px 0;
  transform: rotate(0deg) scale(1);
}

.kole-m-checkbox__label {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
}

/* 说明行:标签下的一行小字 */
.kole-m-checkbox__desc {
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  font-size: var(--kole-m-font-size-label);
  color: var(--kole-color-text-secondary);
}

/* 全选行:与普通项同一行结构,仅用一条分隔线把它和明细分开 */
.kole-m-checkbox--all {
  border-bottom: 1px solid var(--kole-color-border);
  color: var(--kole-color-text-title);
}

/* 变体 button=true:胶囊标签式(筛选条一类的轻量场合,无方框) */
.kole-m-checkbox--button {
  width: auto;
  min-height: var(--kole-m-touch-target);
  padding: var(--kole-space-8) var(--kole-space-16);
  border: 1px solid var(--kole-color-border);
  border-radius: 999px;
  background: var(--kole-color-card-bg);
  color: var(--kole-color-text-secondary);
  font-size: var(--kole-m-font-size-label);
}

.kole-m-checkbox--button .kole-m-checkbox__icon { display: none; }

.kole-m-checkbox--button.is-checked {
  border-color: var(--kole-color-brand);
  background: var(--kole-color-brand-bg);
  color: var(--kole-color-brand);
}

/* 状态 disabled:置灰且不可聚焦(原生 disabled) */
.kole-m-checkbox:disabled {
  color: var(--kole-color-text-disabled);
  background: var(--kole-color-card-bg);
  cursor: not-allowed;
}

.kole-m-checkbox:disabled .kole-m-checkbox__icon {
  border-color: var(--kole-color-text-disabled);
  background: var(--kole-color-disabled-bg);
}

.kole-m-checkbox--button:disabled {
  border-color: var(--kole-color-border);
  background: var(--kole-color-disabled-bg);
  color: var(--kole-color-text-disabled);
}

.kole-m-checkbox:focus-visible {
  outline: 2px solid var(--kole-color-focus-ring);
  outline-offset: -2px;
}
frameworks-mobile/Checkbox.html · H5 原生(无框架) · 210 行
frameworks-mobile/Checkbox.html
<!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 · Checkbox(H5)</title>
<link rel="stylesheet" href="../.design_library/kole-ui-mobile/colors_and_type.css">
<link rel="stylesheet" href="Checkbox.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-pad { padding: var(--kole-m-gutter); 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">复选框组(多选:各项互不影响;点一项即切换该项的选中态)</p>
    <div class="kole-m-checkbox-group" role="group" aria-label="兴趣标签" id="cb-basic" data-value="news,market" data-assert="checkbox-basic">
      <button class="kole-m-checkbox is-checked" type="button" role="checkbox" aria-checked="true" data-checkbox="news">
        <span class="kole-m-checkbox__icon" aria-hidden="true"></span>
        <span class="kole-m-checkbox__label">行业新闻</span>
      </button>
      <button class="kole-m-checkbox" type="button" role="checkbox" aria-checked="false" data-checkbox="report"
              data-behavior="click-sets-attr:#cb-basic|data-value|news,report,market">
        <span class="kole-m-checkbox__icon" aria-hidden="true"></span>
        <span class="kole-m-checkbox__label">数据报告</span>
      </button>
      <button class="kole-m-checkbox is-checked" type="button" role="checkbox" aria-checked="true" data-checkbox="market">
        <span class="kole-m-checkbox__icon" aria-hidden="true"></span>
        <span class="kole-m-checkbox__label">营销活动</span>
      </button>
    </div>
  </section>

  <section class="demo-block" data-demo="states">
    <p class="demo-label">三种选中态(未选中 / 选中 / 半选,全选行的半选由组内各项推导)</p>
    <div class="demo-pad" data-assert="checkbox-states">
      <div class="kole-m-checkbox-group">
        <button class="kole-m-checkbox" type="button" role="checkbox" aria-checked="false">
          <span class="kole-m-checkbox__icon" aria-hidden="true"></span>
          <span class="kole-m-checkbox__label">未选中:空心方框</span>
        </button>
        <button class="kole-m-checkbox is-checked" type="button" role="checkbox" aria-checked="true">
          <span class="kole-m-checkbox__icon" aria-hidden="true"></span>
          <span class="kole-m-checkbox__label">选中:品牌底 + 反色勾号</span>
        </button>
        <button class="kole-m-checkbox is-indeterminate" type="button" role="checkbox" aria-checked="mixed" id="cb-mixed"
                data-behavior="click-toggles-class:#cb-mixed|is-indeterminate">
          <span class="kole-m-checkbox__icon" aria-hidden="true"></span>
          <span class="kole-m-checkbox__label">半选:横杠(组内部分选中时)</span>
        </button>
      </div>
    </div>
  </section>

  <section class="demo-block" data-demo="all">
    <p class="demo-label">全选与半选(全选行不存储自己的状态:全选 / 半选 / 空都由下面三项推导)</p>
    <div class="kole-m-checkbox-group" role="group" aria-label="消息通知" id="cb-all"
         data-value="system,order" data-assert="checkbox-all">
      <button class="kole-m-checkbox kole-m-checkbox--all is-indeterminate" type="button" role="checkbox"
              aria-checked="mixed" id="cb-all-toggle" data-checkbox="__all__"
              data-behavior="click-sets-attr:#cb-all|data-value|system,order,promo">
        <span class="kole-m-checkbox__icon" aria-hidden="true"></span>
        <span class="kole-m-checkbox__label">全选</span>
      </button>
      <button class="kole-m-checkbox is-checked" type="button" role="checkbox" aria-checked="true" data-checkbox="system">
        <span class="kole-m-checkbox__icon" aria-hidden="true"></span>
        <span class="kole-m-checkbox__label">系统消息</span>
      </button>
      <button class="kole-m-checkbox is-checked" type="button" role="checkbox" aria-checked="true" data-checkbox="order">
        <span class="kole-m-checkbox__icon" aria-hidden="true"></span>
        <span class="kole-m-checkbox__label">订单消息</span>
      </button>
      <button class="kole-m-checkbox" type="button" role="checkbox" aria-checked="false" data-checkbox="promo">
        <span class="kole-m-checkbox__icon" aria-hidden="true"></span>
        <span class="kole-m-checkbox__label">营销推送</span>
      </button>
    </div>
  </section>

  <section class="demo-block" data-demo="horizontal">
    <p class="demo-label">横向排列(orientation=horizontal:值少、文案短时用,不再画分隔线)</p>
    <div class="demo-pad">
      <div class="kole-m-checkbox-group kole-m-checkbox-group--horizontal" role="group" aria-label="筛选维度"
           id="cb-horizontal" data-value="brand" data-assert="checkbox-horizontal">
        <button class="kole-m-checkbox is-checked" type="button" role="checkbox" aria-checked="true" data-checkbox="brand">
          <span class="kole-m-checkbox__icon" aria-hidden="true"></span>
          <span class="kole-m-checkbox__label">品牌</span>
        </button>
        <button class="kole-m-checkbox" type="button" role="checkbox" aria-checked="false" data-checkbox="price"
                data-behavior="click-sets-attr:#cb-horizontal|data-value|brand,price">
          <span class="kole-m-checkbox__icon" aria-hidden="true"></span>
          <span class="kole-m-checkbox__label">价格</span>
        </button>
        <button class="kole-m-checkbox" type="button" role="checkbox" aria-checked="false" data-checkbox="stock">
          <span class="kole-m-checkbox__icon" aria-hidden="true"></span>
          <span class="kole-m-checkbox__label">仅看有货</span>
        </button>
      </div>
    </div>
  </section>

  <section class="demo-block" data-demo="disabled">
    <p class="demo-label">禁用项(置灰且不响应:原生 disabled 让读屏播报不可用、键盘跳过)</p>
    <div class="kole-m-checkbox-group" role="group" aria-label="订阅范围" data-assert="checkbox-disabled">
      <button class="kole-m-checkbox is-checked" type="button" role="checkbox" aria-checked="true">
        <span class="kole-m-checkbox__icon" aria-hidden="true"></span>
        <span class="kole-m-checkbox__label">站内信</span>
      </button>
      <button class="kole-m-checkbox" type="button" role="checkbox" aria-checked="false" aria-disabled="true" disabled>
        <span class="kole-m-checkbox__icon" aria-hidden="true"></span>
        <span class="kole-m-checkbox__label">
          短信提醒
          <span class="kole-m-checkbox__desc">需先绑定手机号</span>
        </span>
      </button>
    </div>
  </section>
</div>
<script>
  /* 演示页脚本:真实多选 + 全选推导。
     - 点普通项:切换该项 is-checked + aria-checked,然后按组内各项重算全选行的三态
     - 点全选行(data-checkbox="__all__"):全未选中时全选、否则全部清空
     - 组根写 data-value(逗号分隔的当前值),供宿主持久化
     真实业务里这份状态由宿主管理(受控 value 数组 + change 事件),此处是最小可运行实现。 */
  (function () {
    function itemsOf(group) {
      return Array.prototype.slice.call(group.querySelectorAll('.kole-m-checkbox')).filter(function (it) {
        return it.getAttribute('data-checkbox') !== '__all__';
      });
    }

    function apply(el, on) {
      el.classList.toggle('is-checked', on);
      el.classList.remove('is-indeterminate');
      el.setAttribute('aria-checked', on ? 'true' : 'false');
    }

    function syncAll(group) {
      var all = group.querySelector('.kole-m-checkbox--all');
      if (!all) return;
      var items = itemsOf(group);
      var on = items.filter(function (it) { return it.classList.contains('is-checked'); }).length;
      all.classList.toggle('is-checked', on === items.length && items.length > 0);
      all.classList.toggle('is-indeterminate', on > 0 && on < items.length);
      all.setAttribute('aria-checked', on === 0 ? 'false' : on === items.length ? 'true' : 'mixed');
    }

    function syncValue(group) {
      var picked = itemsOf(group)
        .filter(function (it) { return it.classList.contains('is-checked'); })
        .map(function (it) { return it.getAttribute('data-checkbox'); });
      group.setAttribute('data-value', picked.join(','));
    }

    Array.prototype.forEach.call(document.querySelectorAll('.kole-m-checkbox-group'), function (group) {
      group.addEventListener('click', function (e) {
        var btn = e.target.closest ? e.target.closest('.kole-m-checkbox') : null;
        if (!btn || !group.contains(btn)) return;
        if (btn.disabled || btn.getAttribute('aria-disabled') === 'true') return;
        var items = itemsOf(group);
        if (btn.getAttribute('data-checkbox') === '__all__') {
          var anyOff = items.some(function (it) { return !it.classList.contains('is-checked'); });
          items.forEach(function (it) { apply(it, anyOff); });
        } else {
          apply(btn, !btn.classList.contains('is-checked'));
        }
        syncAll(group);
        syncValue(group);
      });
      syncAll(group);
      syncValue(group);
    });

    /* 独立胶囊按钮(不组成 group):点一下切自己的选中态 */
    Array.prototype.forEach.call(document.querySelectorAll('.kole-m-checkbox--button'), function (btn) {
      if (btn.closest('.kole-m-checkbox-group') || btn.disabled) return;
      btn.addEventListener('click', function () {
        apply(btn, !btn.classList.contains('is-checked'));
      });
    });
  })();
</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/Checkbox.jsx · React · 98 行
frameworks-mobile/Checkbox.jsx
import React from 'react';
import './Checkbox.css';

/* 多选框(移动端)— 规格 §34
   一组里可以同时选中多项;勾选态用「勾号出现/消失」+ aria-checked 双通道表达(不只靠颜色)。
   含全选形态:全选行不存储自己的状态,而是由组内各项推导出「全选 / 半选 / 空」三态
   (半选写 aria-checked="mixed",这是 checkbox 角色允许的第三个值)。
   整行(方框 + 文字)都是 44px 高的热区。本端只回传下一组值,是否采纳由宿主决定。 */

export default function Checkbox({
  options = [],
  value = [],
  orientation = 'vertical',
  button = false,
  selectAll = false,
  disabled = false,
  label = '多选项',
  onChange,
  children = null,
}) {
  const picked = Array.isArray(value) ? value : [];
  const chosen = options.filter((o) => o && picked.indexOf(o.value) >= 0);

  const cls =
    'kole-m-checkbox-group' +
    (orientation === 'horizontal' ? ' kole-m-checkbox-group--horizontal' : '') +
    (disabled ? ' is-disabled' : '');

  function toggle(key) {
    if (disabled) return;
    const has = picked.indexOf(key) >= 0;
    const next = has ? picked.filter((k) => k !== key) : picked.concat([key]);
    if (onChange) onChange(next);
  }

  function toggleAll() {
    if (disabled) return;
    /* 只要还有没选中的就全选,否则全部清空(与演示页同一判据) */
    const anyOff = options.some((o) => o && picked.indexOf(o.value) < 0 && !o.disabled);
    const next = anyOff ? options.filter((o) => o && !o.disabled).map((o) => o.value) : [];
    if (onChange) onChange(next);
  }

  const allState = chosen.length === 0 ? 'false' : chosen.length === options.length ? 'true' : 'mixed';

  return (
    <div className={cls} role="group" aria-label={label}>
      {selectAll ? (
        <button
          className={
            'kole-m-checkbox kole-m-checkbox--all' +
            (allState === 'true' ? ' is-checked' : '') +
            (allState === 'mixed' ? ' is-indeterminate' : '')
          }
          type="button"
          role="checkbox"
          aria-checked={allState}
          disabled={disabled}
          onClick={toggleAll}
        >
          <span className="kole-m-checkbox__icon" aria-hidden="true" />
          <span className="kole-m-checkbox__label">全选</span>
        </button>
      ) : null}
      {options.map((opt, i) => {
        const item = opt || {};
        const key = item.value !== undefined ? item.value : String(i);
        const checked = picked.indexOf(key) >= 0;
        return (
          <button
            className={
              'kole-m-checkbox' + (button ? ' kole-m-checkbox--button' : '') + (checked ? ' is-checked' : '')
            }
            key={String(key)}
            type="button"
            role="checkbox"
            aria-checked={checked ? 'true' : 'false'}
            aria-disabled={item.disabled || disabled ? 'true' : undefined}
            disabled={!!(item.disabled || disabled)}
            onClick={() => {
              if (item.disabled) return;
              toggle(key);
            }}
          >
            {/* 方框:装饰性(语义由 role="checkbox" + aria-checked 承担) */}
            <span className="kole-m-checkbox__icon" aria-hidden="true" />
            <span className="kole-m-checkbox__label">
              {item.label}
              {item.desc ? <span className="kole-m-checkbox__desc">{item.desc}</span> : null}
            </span>
          </button>
        );
      })}
      {children}
    </div>
  );
}
frameworks-mobile/Checkbox.vue2.vue · Vue 2 · 103 行
frameworks-mobile/Checkbox.vue2.vue
<template>
  <div class="kole-m-checkbox-group" :class="groupClass" role="group" :aria-label="label">
    <button
      v-if="selectAll"
      class="kole-m-checkbox kole-m-checkbox--all"
      :class="{ 'is-checked': allState === 'true', 'is-indeterminate': allState === 'mixed' }"
      type="button"
      role="checkbox"
      :aria-checked="allState"
      :disabled="disabled"
      @click="toggleAll"
    >
      <span class="kole-m-checkbox__icon" aria-hidden="true"></span>
      <span class="kole-m-checkbox__label">全选</span>
    </button>
    <button
      v-for="(item, i) in options"
      :key="String(keyOf(item, i))"
      class="kole-m-checkbox"
      :class="{ 'kole-m-checkbox--button': button, 'is-checked': isPicked(item, i) }"
      type="button"
      role="checkbox"
      :aria-checked="isPicked(item, i) ? 'true' : 'false'"
      :aria-disabled="item && (item.disabled || disabled) ? 'true' : null"
      :disabled="!!(item && (item.disabled || disabled))"
      @click="toggle(item, i)"
    >
      <span class="kole-m-checkbox__icon" aria-hidden="true"></span>
      <span class="kole-m-checkbox__label">
        {{ item && item.label }}
        <span v-if="item && item.desc" class="kole-m-checkbox__desc">{{ item.desc }}</span>
      </span>
    </button>
    <slot></slot>
  </div>
</template>

<script>
/* 多选框(移动端)— 规格 §34
   一组里可以同时选中多项;勾选态用「勾号出现/消失」+ aria-checked 双通道表达(不只靠颜色)。
   含全选形态:全选行不存储自己的状态,而是由组内各项推导出「全选 / 半选 / 空」三态
   (半选写 aria-checked="mixed",这是 checkbox 角色允许的第三个值)。
   整行(方框 + 文字)都是 44px 高的热区。本端只回传下一组值,是否采纳由宿主决定。 */

export default {
  name: 'KoleMCheckbox',
  props: {
    options: { type: Array, default: function () { return []; } },
    value: { type: Array, default: function () { return []; } },
    orientation: { type: String, default: 'vertical' },
    button: { type: Boolean, default: false },
    selectAll: { type: Boolean, default: false },
    disabled: { type: Boolean, default: false },
    label: { type: String, default: '多选项' }
  },
  computed: {
    groupClass: function () {
      return [
        this.orientation === 'horizontal' ? 'kole-m-checkbox-group--horizontal' : '',
        this.disabled ? 'is-disabled' : ''
      ].filter(Boolean);
    },
    allState: function () {
      var total = this.options.length;
      var picked = 0;
      for (var i = 0; i < total; i++) if (this.isPicked(this.options[i], i)) picked++;
      if (picked === 0) return 'false';
      return picked === total ? 'true' : 'mixed';
    }
  },
  methods: {
    keyOf: function (item, i) {
      return item && item.value !== undefined ? item.value : String(i);
    },
    isPicked: function (item, i) {
      return this.value.indexOf(this.keyOf(item, i)) >= 0;
    },
    toggle: function (item, i) {
      if (this.disabled || (item && item.disabled)) return;
      var key = this.keyOf(item, i);
      var has = this.value.indexOf(key) >= 0;
      this.$emit('change', has ? this.value.filter(function (k) { return k !== key; }) : this.value.concat([key]));
    },
    toggleAll: function () {
      var self = this;
      if (this.disabled) return;
      /* 只要还有没选中的就全选,否则全部清空(与演示页同一判据) */
      var anyOff = this.options.some(function (o, i) {
        return !(o && o.disabled) && !self.isPicked(o, i);
      });
      this.$emit(
        'change',
        anyOff
          ? this.options.filter(function (o) { return o && !o.disabled; }).map(function (o) { return o.value || ''; })
          : []
      );
    }
  }
};
</script>

<style src="./Checkbox.css"></style>
frameworks-mobile/Checkbox.vue3.vue · Vue 3 · 89 行
frameworks-mobile/Checkbox.vue3.vue
<template>
  <div class="kole-m-checkbox-group" :class="groupClass" role="group" :aria-label="label">
    <button
      v-if="selectAll"
      class="kole-m-checkbox kole-m-checkbox--all"
      :class="{ 'is-checked': allState === 'true', 'is-indeterminate': allState === 'mixed' }"
      type="button"
      role="checkbox"
      :aria-checked="allState"
      :disabled="disabled"
      @click="toggleAll"
    >
      <span class="kole-m-checkbox__icon" aria-hidden="true"></span>
      <span class="kole-m-checkbox__label">全选</span>
    </button>
    <button
      v-for="(item, i) in options"
      :key="String(keyOf(item, i))"
      class="kole-m-checkbox"
      :class="{ 'kole-m-checkbox--button': button, 'is-checked': isPicked(item, i) }"
      type="button"
      role="checkbox"
      :aria-checked="isPicked(item, i) ? 'true' : 'false'"
      :aria-disabled="item && (item.disabled || disabled) ? 'true' : null"
      :disabled="!!(item && (item.disabled || disabled))"
      @click="toggle(item, i)"
    >
      <span class="kole-m-checkbox__icon" aria-hidden="true"></span>
      <span class="kole-m-checkbox__label">
        {{ item && item.label }}
        <span v-if="item && item.desc" class="kole-m-checkbox__desc">{{ item.desc }}</span>
      </span>
    </button>
    <slot></slot>
  </div>
</template>

<script setup>
/* 多选框(移动端)— 规格 §34
   一组里可以同时选中多项;勾选态用「勾号出现/消失」+ aria-checked 双通道表达(不只靠颜色)。
   含全选形态:全选行不存储自己的状态,而是由组内各项推导出「全选 / 半选 / 空」三态
   (半选写 aria-checked="mixed",这是 checkbox 角色允许的第三个值)。
   整行(方框 + 文字)都是 44px 高的热区。本端只回传下一组值,是否采纳由宿主决定。 */
import { computed } from 'vue';

const props = defineProps({
  options: { type: Array, default: () => [] },
  value: { type: Array, default: () => [] },
  orientation: { type: String, default: 'vertical' },
  button: { type: Boolean, default: false },
  selectAll: { type: Boolean, default: false },
  disabled: { type: Boolean, default: false },
  label: { type: String, default: '多选项' }
});
const emit = defineEmits(['change']);

const groupClass = computed(() => [
  props.orientation === 'horizontal' ? 'kole-m-checkbox-group--horizontal' : '',
  props.disabled ? 'is-disabled' : ''
].filter(Boolean));

function keyOf(item, i) {
  return item && item.value !== undefined ? item.value : String(i);
}
function isPicked(item, i) {
  return props.value.indexOf(keyOf(item, i)) >= 0;
}
function toggle(item, i) {
  if (props.disabled || (item && item.disabled)) return;
  const key = keyOf(item, i);
  const has = props.value.indexOf(key) >= 0;
  emit('change', has ? props.value.filter((k) => k !== key) : props.value.concat([key]));
}
const allState = computed(() => {
  const total = props.options.length;
  const picked = props.options.filter((o, i) => isPicked(o, i)).length;
  if (picked === 0) return 'false';
  return picked === total ? 'true' : 'mixed';
});
function toggleAll() {
  if (props.disabled) return;
  /* 只要还有没选中的就全选,否则全部清空(与演示页同一判据) */
  const anyOff = props.options.some((o, i) => !(o && o.disabled) && !isPicked(o, i));
  emit('change', anyOff ? props.options.filter((o) => o && !o.disabled).map((o) => o.value || '') : []);
}
</script>

<style src="./Checkbox.css"></style>
frameworks-mobile/Checkbox.uniapp.vue · uni-app(跨端:小程序 / App / H5) · 230 行
frameworks-mobile/Checkbox.uniapp.vue
<template>
  <view class="kole-m-checkbox-group" :class="groupClass" role="group" :aria-label="label">
    <view
      v-if="selectAll"
      class="kole-m-checkbox kole-m-checkbox--all"
      :class="{ 'is-checked': allState === 'true', 'is-indeterminate': allState === 'mixed' }"
      :role="disabled ? '' : 'checkbox'"
      :aria-checked="allState"
      :aria-disabled="disabled ? 'true' : 'false'"
      @tap="toggleAll"
    >
      <view class="kole-m-checkbox__icon" />
      <view class="kole-m-checkbox__label"><text>全选</text></view>
    </view>
    <view
      v-for="(item, i) in options"
      :key="String(keyOf(item, i))"
      class="kole-m-checkbox"
      :class="{ 'kole-m-checkbox--button': button, 'is-checked': isPicked(item, i) }"
      :role="item && (item.disabled || disabled) ? '' : 'checkbox'"
      :aria-checked="isPicked(item, i) ? 'true' : 'false'"
      :aria-disabled="item && (item.disabled || disabled) ? 'true' : 'false'"
      @tap="toggle(item, i)"
    >
      <view class="kole-m-checkbox__icon" />
      <view class="kole-m-checkbox__label">
        <text>{{ item && item.label }}</text>
        <text v-if="item && item.desc" class="kole-m-checkbox__desc">{{ item.desc }}</text>
      </view>
    </view>
    <slot></slot>
  </view>
</template>

<script setup>
/* uni-app 端 · 多选框(移动端)— 规格 §34
   跨端差异:
   ① 项用 view + role="checkbox"(小程序没有可聚焦的原生 button 语义差异),点击用 @tap;
   ② 勾号用 view::after 两条边框拼(不引入图标字体),半选态改成一条横杠;
   ③ 禁用不用原生 disabled(view 没有),改为 aria-disabled + 代码拦截点击;
   ④ 全选行的三态由组内各项推导,半选写 aria-checked="mixed"。
   尺寸用 rpx:88rpx = 375pt 下的 44px 触控最小边长,方框 40rpx = 20px。 */
import { computed } from 'vue';

const props = defineProps({
  options: { type: Array, default: () => [] },
  value: { type: Array, default: () => [] },
  orientation: { type: String, default: 'vertical' },
  button: { type: Boolean, default: false },
  selectAll: { type: Boolean, default: false },
  disabled: { type: Boolean, default: false },
  label: { type: String, default: '多选项' }
});
const emit = defineEmits(['change']);

const groupClass = computed(() => [
  props.orientation === 'horizontal' ? 'kole-m-checkbox-group--horizontal' : '',
  props.disabled ? 'is-disabled' : ''
].filter(Boolean));

function keyOf(item, i) {
  return item && item.value !== undefined ? item.value : String(i);
}
function isPicked(item, i) {
  return props.value.indexOf(keyOf(item, i)) >= 0;
}
function toggle(item, i) {
  if (props.disabled || (item && item.disabled)) return;
  const key = keyOf(item, i);
  const has = props.value.indexOf(key) >= 0;
  emit('change', has ? props.value.filter((k) => k !== key) : props.value.concat([key]));
}
const allState = computed(() => {
  const total = props.options.length;
  const picked = props.options.filter((o, i) => isPicked(o, i)).length;
  if (picked === 0) return 'false';
  return picked === total ? 'true' : 'mixed';
});
function toggleAll() {
  if (props.disabled) return;
  /* 只要还有没选中的就全选,否则全部清空(与演示页同一判据) */
  const anyOff = props.options.some((o, i) => !(o && o.disabled) && !isPicked(o, i));
  emit('change', anyOff ? props.options.filter((o) => o && !o.disabled).map((o) => o.value || '') : []);
}
</script>

<style>
.kole-m-checkbox-group {
  --kole-m-checkbox-size: 40rpx;   /* 方框边长(20px @375pt) */
  --kole-m-checkbox-gap: 24rpx;    /* 方框与文字的间距 */
  --kole-m-touch-target: 88rpx;
  --kole-m-font-size-body: 32rpx;
  --kole-m-font-size-label: 28rpx;
  --kole-m-gutter: 32rpx;
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  width: 100%;
  background-color: var(--kole-color-card-bg);
  color: var(--kole-color-text-body);
  font-size: var(--kole-m-font-size-body);
}

.kole-m-checkbox-group--horizontal {
  flex-direction: row;
  flex-wrap: wrap;
  align-items: center;
}

.kole-m-checkbox-group:not(.kole-m-checkbox-group--horizontal) .kole-m-checkbox + .kole-m-checkbox {
  border-top: 1rpx solid var(--kole-color-border);
}

.kole-m-checkbox {
  box-sizing: border-box;
  display: flex;
  align-items: center;
  width: 100%;
  min-height: var(--kole-m-touch-target);
  padding: 24rpx var(--kole-m-gutter);
  background-color: var(--kole-color-card-bg);
  color: var(--kole-color-text-body);
  font-size: inherit;
}

.kole-m-checkbox-group--horizontal .kole-m-checkbox {
  width: auto;
  padding-left: 24rpx;
  padding-right: 24rpx;
}

.kole-m-checkbox:active { background-color: var(--kole-color-table-header-bg); }

/* 方框:未选中空心,选中品牌底 + 反色勾号(勾号用两条边框拼,不引图标字体) */
.kole-m-checkbox__icon {
  position: relative;
  flex-shrink: 0;
  box-sizing: border-box;
  width: var(--kole-m-checkbox-size);
  height: var(--kole-m-checkbox-size);
  margin-right: var(--kole-m-checkbox-gap);
  border: 1rpx solid var(--kole-color-text-placeholder);
  border-radius: 4rpx;
  background-color: var(--kole-color-card-bg);
}

.kole-m-checkbox__icon::after {
  content: '';
  position: absolute;
  top: 6rpx;
  left: 12rpx;
  width: 10rpx;
  height: 18rpx;
  border-color: var(--kole-color-text-inverse);
  border-style: solid;
  border-width: 0 4rpx 4rpx 0;
  transform: rotate(45deg) scale(0);
}

.kole-m-checkbox.is-checked .kole-m-checkbox__icon {
  border-color: var(--kole-color-brand);
  background-color: var(--kole-color-brand);
}

.kole-m-checkbox.is-checked .kole-m-checkbox__icon::after { transform: rotate(45deg) scale(1); }

/* 状态 indeterminate(半选):品牌底 + 一条横杠 */
.kole-m-checkbox.is-indeterminate .kole-m-checkbox__icon {
  border-color: var(--kole-color-brand);
  background-color: var(--kole-color-brand);
}

.kole-m-checkbox.is-indeterminate .kole-m-checkbox__icon::after {
  top: 16rpx;
  left: 6rpx;
  width: 20rpx;
  height: 0;
  border-width: 0 0 4rpx 0;
  transform: rotate(0deg) scale(1);
}

.kole-m-checkbox__label { flex: 1; display: flex; flex-direction: column; }

.kole-m-checkbox__desc {
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  font-size: var(--kole-m-font-size-label);
  color: var(--kole-color-text-secondary);
}

/* 全选行:与普通项同一行结构,仅用一条分隔线把它和明细分开 */
.kole-m-checkbox--all {
  border-bottom: 1rpx solid var(--kole-color-border);
  color: var(--kole-color-text-title);
}

/* 变体 button=true:胶囊标签式(无方框) */
.kole-m-checkbox--button {
  width: auto;
  padding: 16rpx 32rpx;
  border: 1rpx solid var(--kole-color-border);
  border-radius: 999rpx;
  color: var(--kole-color-text-secondary);
  font-size: var(--kole-m-font-size-label);
}

.kole-m-checkbox--button .kole-m-checkbox__icon { display: none; }

.kole-m-checkbox--button.is-checked {
  border-color: var(--kole-color-brand);
  background-color: var(--kole-color-brand-bg);
  color: var(--kole-color-brand);
}

/* 状态 disabled:置灰且不响应 */
.kole-m-checkbox[aria-disabled='true'] { color: var(--kole-color-text-disabled); }

.kole-m-checkbox[aria-disabled='true'] .kole-m-checkbox__icon {
  border-color: var(--kole-color-text-disabled);
  background-color: var(--kole-color-disabled-bg);
}

.kole-m-checkbox--button[aria-disabled='true'] {
  border-color: var(--kole-color-border);
  background-color: var(--kole-color-disabled-bg);
  color: var(--kole-color-text-disabled);
}
</style>

测试与回归

断言在真实的 375×640 设备帧里跑(引擎与 PC 侧共用 tests/_runtime.js,触控行为动词来自移动端 tests/mobile/_behaviors.js)。

断言 19 条 · 全部通过 报告 2026-09-20 16:50:33

复现命令
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-checkbox.json(点击展开原始 JSON)
{
  "schemaVersion": 1,
  "sourceKind": "authored-spec",
  "provenance": "authored-in-repo",
  "specFile": "spec/移动端规格.md",
  "specSection": "34 · 多选框 Checkbox",
  "confidence": "high",
  "slug": "mobile-checkbox",
  "name": "多选框 Checkbox",
  "semanticTypeCandidates": [
    "checkbox",
    "checkbox-group",
    "multi-choice"
  ],
  "variantDimensions": [
    {
      "name": "orientation",
      "values": [
        "vertical",
        "horizontal"
      ]
    },
    {
      "name": "button",
      "values": [
        "false",
        "true"
      ]
    },
    {
      "name": "selectAll",
      "values": [
        "false",
        "true"
      ]
    }
  ],
  "representativeVariants": [
    {
      "orientation": "vertical",
      "button": "false",
      "selectAll": "false",
      "label": "默认(方框 + 文字,纵向排列并画分隔线)"
    },
    {
      "orientation": "vertical",
      "button": "false",
      "selectAll": "true",
      "label": "带全选行(组首一行「全选」,三态由组内推导)"
    },
    {
      "orientation": "horizontal",
      "button": "true",
      "selectAll": "false",
      "label": "胶囊标签式横向(无方框,筛选条用)"
    }
  ],
  "anatomy": {
    "group": "根元素,role=\"group\" + aria-label 说明这组在选什么",
    "item": "单个选项,整行都是热区(原生 button + role=\"checkbox\")",
    "icon": "方框,未选中是空心框、选中是品牌底 + 反色勾号、半选是品牌底 + 横杠",
    "label": "选项文字,占满剩余宽度",
    "desc": "可选说明行,跟在文字下方(如「需先绑定手机号」)",
    "all": "全选行,不存储自己的状态,由组内各项推导"
  },
  "structurePatterns": {
    "orientation": "vertical(纵向排列,组内画分隔线)/ horizontal(横向排列,靠间距分组)",
    "button": "false(方框 + 文字)/ true(胶囊标签式,无方框)",
    "selectAll": "false(不显示全选行)/ true(组首显示全选行)"
  },
  "usageHints": [
    "在一组选项里同时选中任意多项(兴趣标签、订阅范围、筛选维度),并支持「全选」",
    "触屏上没有鼠标悬停预告状态,全选行在「部分选中」时必须显示出与「全选 / 全不选」都不同的第三态(横杠)",
    "整行(方框 + 文字)都是热区,行高不小于 44px",
    "点全选行:只要还有未选中项就全部选中,否则全部清空(不在「半选」上停留)",
    "全选的选中态是推导值,不单独存储 —— 单项变化后立即重算"
  ],
  "doNotInvent": [
    "「最多选 N 项」的数量上限与超出提示(业务规则在宿主)",
    "分组嵌套(一组里再分组)的层级表达",
    "与表单一起提交时的隐藏字段(由宿主添加)",
    "选项内容的异步加载与「已选 N 项」的汇总条"
  ],
  "unknowns": [
    "全选行是否显示「已选 2/5」这类计数",
    "半选态在非全选行上的用例(如父级节点)",
    "胶囊按钮式是否也需要禁用态的视觉层(当前复用同一条置灰规则)"
  ],
  "interaction": [
    "整行(方框 + 文字)都是热区,行高不小于 44px;横向组里每项自身也保持这个边长",
    "一次触摸即切换,各项互相独立;不限制同时选中的数量上限",
    "点全选行:只要还有未选中项就全部选中,否则全部清空(不在「半选」上停留)",
    "全选的选中态是**推导值**,不单独存储 —— 单项变化后立即重算,避免出现「全选已勾上但还有一项没选」",
    "切换动效是勾号 150ms 缩放;减少动态偏好下瞬时切换",
    "每次切换立即触发 change 事件,回传切换后的完整值数组"
  ],
  "accessibility": [
    "组用 role=\"group\" + aria-label 说明分组名称",
    "每项是原生 button + role=\"checkbox\" + aria-checked(读屏会播报「复选框,已选中/未选中」)",
    "全选行的半选态用 aria-checked=\"mixed\"(checkbox 角色允许的第三个值)",
    "方框与勾号是纯装饰,对读屏隐藏(aria-hidden),语义全靠 role=\"checkbox\"",
    "禁用项用原生 disabled 并补 aria-disabled=\"true\""
  ],
  "api": {
    "source": "implementation",
    "note": "props / events / slots 为 6 端实现的公共接口(说明文字取自规格对应小节)。字段名与各端源码逐名核对:node tools/verify-mobile-docs.mjs",
    "requiredNote": "「必传」按严格定义:实现里**没有默认值**时才为 Y(本门禁逐条核对 props 与各端源码的默认值,防止契约与实现脱节)。",
    "props": [
      {
        "name": "options",
        "type": "Array",
        "default": "[]",
        "desc": "选项数组,每项 { value, label, desc, disabled }(规格 §34.2)",
        "required": false
      },
      {
        "name": "value",
        "type": "Array",
        "default": "[]",
        "desc": "受控选中值数组,每项与 aria-checked 同步写(规格 §34.5)",
        "required": false
      },
      {
        "name": "orientation",
        "type": "'vertical' | 'horizontal'",
        "default": "'vertical'",
        "desc": "变体 orientation:纵向(画分隔线)还是横向排列(规格 §34.3)",
        "required": false
      },
      {
        "name": "button",
        "type": "boolean",
        "default": "false",
        "desc": "变体 button:胶囊标签式(无方框),用于筛选条(规格 §34.3)",
        "required": false
      },
      {
        "name": "selectAll",
        "type": "boolean",
        "default": "false",
        "desc": "变体 selectAll:组首是否显示全选行(规格 §34.3)",
        "required": false
      },
      {
        "name": "disabled",
        "type": "boolean",
        "default": "false",
        "desc": "状态 disabled:整组置灰且不可聚焦(规格 §34.4)",
        "required": false
      },
      {
        "name": "label",
        "type": "string",
        "default": "'多选项'",
        "desc": "分组名称,落到 group 的 aria-label(规格 §34.6)",
        "required": false
      }
    ],
    "events": [
      {
        "name": "change",
        "params": "(value)",
        "desc": "切换任一项或全选时触发,回传切换后的完整值数组(规格 §34.5)"
      }
    ],
    "slots": [
      {
        "name": "default",
        "desc": "额外选项内容(宿主自己渲染的项,追在 options 之后,规格 §34.2 item)"
      }
    ]
  },
  "variantClasses": {
    "orientation": {
      "vertical": [],
      "horizontal": [
        ".kole-m-checkbox-group--horizontal"
      ]
    },
    "button": {
      "false": [],
      "true": [
        ".kole-m-checkbox--button"
      ]
    },
    "selectAll": {
      "false": [],
      "true": [
        ".kole-m-checkbox--all"
      ]
    }
  },
  "demos": [
    {
      "id": "basic",
      "group": "01 组件类型",
      "title": "复选框组",
      "desc": "多选:各项互不影响;点一项即切换该项的选中态。",
      "variant": "orientation=vertical"
    },
    {
      "id": "all",
      "group": "01 组件类型",
      "title": "全选与半选",
      "desc": "全选行不存储自己的状态:全选 / 半选 / 空都由下面各项推导。",
      "variant": "selectAll=true"
    },
    {
      "id": "horizontal",
      "group": "01 组件类型",
      "title": "横向排列",
      "desc": "orientation=horizontal:值少、文案短时用,不再画分隔线。",
      "variant": "orientation=horizontal"
    },
    {
      "id": "states",
      "group": "02 组件状态",
      "title": "三种选中态",
      "desc": "未选中 / 选中 / 半选:勾号出现与消失是形状差异,不只靠颜色。",
      "variant": "状态 checked | indeterminate"
    },
    {
      "id": "disabled",
      "group": "02 组件状态",
      "title": "禁用项",
      "desc": "置灰且不响应:原生 disabled 让读屏播报不可用、键盘跳过。",
      "variant": "disabled=true"
    }
  ],
  "related": [
    {
      "slug": "mobile-radio",
      "why": "选项之间互斥(只能选一项)时用 Radio;可以同时成立多项时才用 Checkbox"
    },
    {
      "slug": "mobile-switch",
      "why": "单个开关类配置用 Switch(即时生效、二值);需要提交一组值时才用 Checkbox 组"
    },
    {
      "slug": "mobile-form",
      "why": "复选框组作为表单里的一个字段时,标签、必填星号与错误提示由 Form 的 item 提供"
    }
  ]
}