颜色选择器ColorPicker
从一组预设色里挑一个颜色(主题色、标签色、看板分类色),或在预设之外补一个自定义色值
数据录入 规格 45 · 颜色选择器 ColorPicker 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-colorpicker.css">
<!-- ③ 结构照抄下方任一演示块(类名与 6 端实现一致) -->
演示
每个演示都是真实渲染:预览帧加载 frameworks-mobile/ColorPicker.html?demo=<id>(只显示该演示块),代码是该演示块在演示页里的原文,可复制。全部演示同屏可看 演示页 ↗。
01 组件类型
mode=swatch:点色块即选中,选中态是外环 + 放大,浅色块上也读得出。
查看代码(演示页原文 · 28 行)
<section class="demo-block" data-demo="swatch">
<p class="demo-label">预设色板(mode=swatch:点色块即选中;选中态是外环 + 放大,浅色块上也读得出)</p>
<div class="demo-pad" data-assert="colorpicker-swatch">
<div class="kole-m-colorpicker kole-m-colorpicker--swatch" id="cp-swatch" data-value="#2F54EB"
style="--kole-m-colorpicker-current: #2F54EB" data-assert="colorpicker-swatch-list">
<div class="kole-m-colorpicker__current">
<span class="kole-m-colorpicker__dot" aria-hidden="true"></span>
<span class="kole-m-colorpicker__value" id="cp-swatch-value">#2F54EB</span>
<span class="kole-m-colorpicker__caption">品牌主色</span>
</div>
<div class="kole-m-colorpicker__swatches" role="radiogroup" aria-label="主题色">
<button class="kole-m-colorpicker__swatch is-selected" type="button" role="radio" aria-checked="true"
data-color="#2F54EB" aria-label="#2F54EB" style="--kole-m-colorpicker-swatch: #2F54EB"></button>
<button class="kole-m-colorpicker__swatch" type="button" role="radio" aria-checked="false"
data-color="#13C2C2" aria-label="#13C2C2" style="--kole-m-colorpicker-swatch: #13C2C2"
data-behavior="click-sets-attr:#cp-swatch|data-value|#13C2C2"></button>
<button class="kole-m-colorpicker__swatch" type="button" role="radio" aria-checked="false"
data-color="#2E7D0A" aria-label="#2E7D0A" style="--kole-m-colorpicker-swatch: #2E7D0A"></button>
<button class="kole-m-colorpicker__swatch" type="button" role="radio" aria-checked="false"
data-color="#8C5A00" aria-label="#8C5A00" style="--kole-m-colorpicker-swatch: #8C5A00"></button>
<button class="kole-m-colorpicker__swatch" type="button" role="radio" aria-checked="false"
data-color="#CF1322" aria-label="#CF1322" style="--kole-m-colorpicker-swatch: #CF1322"></button>
<button class="kole-m-colorpicker__swatch" type="button" role="radio" aria-checked="false"
data-color="#1F2329" aria-label="#1F2329" style="--kole-m-colorpicker-swatch: #1F2329"></button>
</div>
</div>
</div>
</section>mode=custom:色板之后追加色值输入,非法值置红并给出提示,不自动纠正。
查看代码(演示页原文 · 27 行)
<section class="demo-block" data-demo="custom">
<p class="demo-label">自定义色值(mode=custom:色板之后追加色值输入;非法值置红并给出提示)</p>
<div class="demo-pad" data-assert="colorpicker-custom">
<div class="kole-m-colorpicker kole-m-colorpicker--custom" id="cp-custom" data-value="#2F54EB"
style="--kole-m-colorpicker-current: #2F54EB">
<div class="kole-m-colorpicker__current">
<span class="kole-m-colorpicker__dot" aria-hidden="true"></span>
<span class="kole-m-colorpicker__value" id="cp-custom-value">#2F54EB</span>
<span class="kole-m-colorpicker__caption">当前色</span>
</div>
<div class="kole-m-colorpicker__swatches" role="radiogroup" aria-label="常用色">
<button class="kole-m-colorpicker__swatch is-selected" type="button" role="radio" aria-checked="true"
data-color="#2F54EB" aria-label="#2F54EB" style="--kole-m-colorpicker-swatch: #2F54EB"></button>
<button class="kole-m-colorpicker__swatch" type="button" role="radio" aria-checked="false"
data-color="#13C2C2" aria-label="#13C2C2" style="--kole-m-colorpicker-swatch: #13C2C2"></button>
<button class="kole-m-colorpicker__swatch" type="button" role="radio" aria-checked="false"
data-color="#2E7D0A" aria-label="#2E7D0A" style="--kole-m-colorpicker-swatch: #2E7D0A"></button>
</div>
<div class="kole-m-colorpicker__custom">
<input class="kole-m-colorpicker__input" id="cp-custom-input" type="text" value="#2F54EB"
aria-label="自定义色值" placeholder="#RRGGBB" autocomplete="off" spellcheck="false">
<button class="kole-m-colorpicker__apply" type="button" id="cp-custom-apply">应用</button>
</div>
<p class="kole-m-colorpicker__error" id="cp-custom-error" hidden>色值需为 #RRGGBB 形式</p>
</div>
</div>
</section>round=true:色块与当前色圆点都用圆形,主题选择器常用。
查看代码(演示页原文 · 22 行)
<section class="demo-block" data-demo="round">
<p class="demo-label">圆点式(round=true:色块与当前色圆点都用圆形,主题选择器常用)</p>
<div class="demo-pad" data-assert="colorpicker-round">
<div class="kole-m-colorpicker kole-m-colorpicker--swatch kole-m-colorpicker--round" id="cp-round"
data-value="#13C2C2" style="--kole-m-colorpicker-current: #13C2C2">
<div class="kole-m-colorpicker__current">
<span class="kole-m-colorpicker__dot" aria-hidden="true"></span>
<span class="kole-m-colorpicker__value">#13C2C2</span>
</div>
<div class="kole-m-colorpicker__swatches" role="radiogroup" aria-label="强调色">
<button class="kole-m-colorpicker__swatch" type="button" role="radio" aria-checked="false"
data-color="#2F54EB" aria-label="#2F54EB" style="--kole-m-colorpicker-swatch: #2F54EB"></button>
<button class="kole-m-colorpicker__swatch is-selected" type="button" role="radio" aria-checked="true"
data-color="#13C2C2" aria-label="#13C2C2" style="--kole-m-colorpicker-swatch: #13C2C2"></button>
<button class="kole-m-colorpicker__swatch" type="button" role="radio" aria-checked="false"
data-color="#2E7D0A" aria-label="#2E7D0A" style="--kole-m-colorpicker-swatch: #2E7D0A"></button>
<button class="kole-m-colorpicker__swatch" type="button" role="radio" aria-checked="false"
data-color="#CF1322" aria-label="#CF1322" style="--kole-m-colorpicker-swatch: #CF1322"></button>
</div>
</div>
</div>
</section>02 组件状态
色值以等宽字呈现,颜色不作为唯一信息通道:读屏与色觉障碍用户读得到色值。
查看代码(演示页原文 · 24 行)
<section class="demo-block" data-demo="value">
<p class="demo-label">当前色显示(色值以等宽字呈现,进读屏的是 aria-label,不靠颜色本身传达信息)</p>
<div class="demo-pad" data-assert="colorpicker-value">
<div class="kole-m-colorpicker kole-m-colorpicker--swatch" id="cp-value"
data-value="#1F2329" style="--kole-m-colorpicker-current: #1F2329">
<div class="kole-m-colorpicker__current">
<span class="kole-m-colorpicker__dot" aria-hidden="true"></span>
<span class="kole-m-colorpicker__value">#1F2329</span>
<span class="kole-m-colorpicker__caption">标题黑</span>
</div>
<div class="kole-m-colorpicker__swatches" role="radiogroup" aria-label="中性色"
style="--kole-m-colorpicker-columns: 4">
<button class="kole-m-colorpicker__swatch" type="button" role="radio" aria-checked="false"
data-color="#CF1322" aria-label="#CF1322" style="--kole-m-colorpicker-swatch: #CF1322"></button>
<button class="kole-m-colorpicker__swatch" type="button" role="radio" aria-checked="false"
data-color="#8C5A00" aria-label="#8C5A00" style="--kole-m-colorpicker-swatch: #8C5A00"></button>
<button class="kole-m-colorpicker__swatch is-selected" type="button" role="radio" aria-checked="true"
data-color="#1F2329" aria-label="#1F2329" style="--kole-m-colorpicker-swatch: #1F2329"></button>
<button class="kole-m-colorpicker__swatch" type="button" role="radio" aria-checked="false"
data-color="#2F54EB" aria-label="#2F54EB" style="--kole-m-colorpicker-swatch: #2F54EB"></button>
</div>
</div>
</div>
</section>整块置灰且色块不可点,读屏播报不可用。
查看代码(演示页原文 · 24 行)
<section class="demo-block" data-demo="disabled">
<p class="demo-label">禁用(状态 disabled:整块置灰、色块不可点;读屏播报不可用)</p>
<div class="demo-pad" data-assert="colorpicker-disabled">
<div class="kole-m-colorpicker kole-m-colorpicker--swatch is-disabled" id="cp-disabled"
data-value="#8C5A00" style="--kole-m-colorpicker-current: #8C5A00">
<div class="kole-m-colorpicker__current">
<span class="kole-m-colorpicker__dot" aria-hidden="true"></span>
<span class="kole-m-colorpicker__value">#8C5A00</span>
<span class="kole-m-colorpicker__caption">已锁定</span>
</div>
<div class="kole-m-colorpicker__swatches" role="radiogroup" aria-label="主题色" aria-disabled="true">
<button class="kole-m-colorpicker__swatch is-selected" type="button" role="radio" aria-checked="true"
data-color="#8C5A00" aria-label="#8C5A00" aria-disabled="true" disabled
style="--kole-m-colorpicker-swatch: #8C5A00"></button>
<button class="kole-m-colorpicker__swatch" type="button" role="radio" aria-checked="false"
data-color="#1F2329" aria-label="#1F2329" aria-disabled="true" disabled
style="--kole-m-colorpicker-swatch: #1F2329"></button>
<button class="kole-m-colorpicker__swatch" type="button" role="radio" aria-checked="false"
data-color="#2F54EB" aria-label="#2F54EB" aria-disabled="true" disabled
style="--kole-m-colorpicker-swatch: #2F54EB"></button>
</div>
</div>
</div>
</section>API
props / events / slots 为 6 端实现的公共接口(说明文字取自规格对应小节)。字段名与各端源码逐名核对:node tools/verify-mobile-docs.mjs
Props
| 名称 | 类型 | 默认值 | 说明 | 必传 |
|---|---|---|---|---|
mode | 'swatch' | 'custom' | 'swatch' | 变体 mode:swatch 只有预设色板,custom 追加自定义色值输入(规格 §45.3) | N |
round | boolean | false | 变体 round:色块与当前色圆点是否用圆形(规格 §45.3) | N |
showValue | boolean | true | 变体 showValue:是否显示色值文字(规格 §45.3) | N |
disabled | boolean | false | 状态 disabled:整块置灰且不响应(规格 §45.4) | N |
value | string | '' | 当前色值,#RRGGBB(规格 §45.6) | N |
presets | Array<string | { value: string, label?: string }> | [] | 预设色板(宿主提供,本组件不发明配色方案)(规格 §45.7) | N |
columns | number | 6 | 色板每行列数,映射到 --kole-m-colorpicker-columns(规格 §45.3) | N |
label | string | '颜色' | 色板 aria-label 与 radiogroup 名称,读屏靠它知道这组在选什么(规格 §45.6) | N |
「必传」按严格定义:实现里**没有默认值**时才为 Y(本门禁逐条核对 props 与各端源码的默认值,防止契约与实现脱节)。
事件
| 名称 | 参数 | 说明 |
|---|---|---|
change | value: string | 点色块选中时触发,回传色值 #RRGGBB(规格 §45.5) |
插槽
| 名称 | 说明 |
|---|---|
default | 自定义内容;mode=custom 时放进 .kole-m-colorpicker__custom(色值输入 + 应用按钮) |
CSS 变量
组件级变量(在组件样式表里定义)。业务侧可在自己的作用域内覆盖,不必改组件源码。
| 名称 | 默认值 | 说明 |
|---|---|---|
--kole-m-colorpicker-swatch-size | 32px | 色块视觉尺寸 |
--kole-m-colorpicker-gap | var(--kole-space-12) | 色块间距 |
--kole-m-colorpicker-columns | 6 | 每行色块数 |
--kole-m-colorpicker-radius | var(--kole-radius-base) | 色块圆角 |
--kole-m-colorpicker-current | var(--kole-color-border) | 组件内部默认值,可在业务侧覆盖 |
何时使用
- 从一组预设色里挑一个颜色(主题色、标签色、看板分类色),或在预设之外补一个自定义色值
- 移动端默认只给预设色板:色值离散可预期;需要自由取色时退化为「输入色值」而不是「拖色相」
- 点色块即选中并回传该色;同组互斥(选新的自动取消旧的)
- 选中反馈是外环 + 轻微放大,不是换色 —— 浅色块(白、浅黄)上换色根本看不出来
- mode=custom 时色值输入只在确认(点「应用」或回车)后生效,输入过程中不回传
- 色值非法时输入框置红并给出错误文案,不自动纠正
交互与触控
- 点色块即选中并回传该色;同组互斥(选新的自动取消旧的)
- 色块视觉 ≥32px、热区按网格列宽铺满,点空白处不响应
- 选中反馈是外环 + scale(1.08),变换用 --kole-duration-fast;减少动态偏好下不放大
- mode=custom 时色值输入只在确认(点「应用」或回车)后生效,输入过程中不回传
- 色值非法时输入框置红并给出错误文案,不自动纠正(不发明「就近取色」这类行为)
无障碍
- 色板 role="radiogroup" + aria-label 说明这组在选什么(如「主题色」)
- 每个色块 role="radio" + aria-checked,aria-label 必须是色值本身(如 #2F54EB)—— 色块没有文字,读屏只能靠 aria-label
- 当前色以等宽文字重复一遍:颜色不作为唯一信息通道(色觉障碍用户读得到色值)
- 禁用块写 aria-disabled="true",读屏播报不可用
相似组件
从「该用哪一个」的角度区分;PC 端的对应实现见 PC 文档站。
| 组件 | 何时用它而不是本组件 |
|---|---|
| 标签Tag | 标签只需要展示颜色(由宿主给背景色),不需要用户去选,用标签而不是颜色选择器 |
| 弹出层Popup | 色板要放进浮层里时用它承载,颜色选择器本身不管浮层 |
| 选择器Picker | 选项是有名字的枚举(主题名:暗色 / 亮色 / 跟随系统)时用选择器,颜色只是它的呈现 |
| 输入框Input | 需要用户手写 rgb() / hsl() 这类非十六进制写法时的兜底输入 |
| 列表List | 颜色以「名称 + 色块」成行罗列时用列表承载,点整行即选 |
规格未定 / 禁止发明
| 类别 | 条目 |
|---|---|
| 禁止发明 | 色相环 / 明度滑条的取色画布(移动端精度不足,本组件不提供) |
| 禁止发明 | 颜色空间转换(RGB / HSL / HSV 互转)与色值自动纠正(只接受 #RRGGBB) |
| 禁止发明 | 主题派生(由品牌色自动生成 hover / active / 浅底等衍生色阶) |
| 禁止发明 | 取色器(吸管)与屏幕取色 |
| 规格未定 | 色板上限(当前不限制数量,但 40 个以上在小屏上会变成一片色噪) |
| 规格未定 | 自定义色值的格式是否要支持 rgb() / hsl() 书写 |
| 规格未定 | 是否需要「最近使用」一行动态色块 |
结构(anatomy)
| 字段 | 说明 |
|---|---|
colorpicker | 根元素,承载当前色与色板 |
colorpicker__current | 当前色显示,由「圆点 + 色值文字」组成 |
colorpicker__dot | 当前色圆点,背景取 --kole-m-colorpicker-current(色值由宿主以行内自定义属性传入) |
colorpicker__value | 色值文字(等宽字),读屏靠 aria-label 而不是色块本身 |
colorpicker__swatches | 色板,role=radiogroup + aria-label,定列数网格 |
colorpicker__swatch | 单个色块,role=radio + aria-checked,背景取 --kole-m-colorpicker-swatch |
colorpicker__custom | 自定义色值输入区(仅 mode=custom),由宿主以插槽填入 |
变体维度与类名映射
类名映射由构建脚本从契约 variantClasses 生成,并被 verify:mobile-docs 逐条对照组件 CSS 校验(类/变量必须真实存在)。
| 维度 | 取值 | 对应类名 / 变量 |
|---|---|---|
mode | swatch / custom | swatch .kole-m-colorpicker--swatch custom .kole-m-colorpicker--custom |
round | false / true | false (由数据驱动,无专属类) true .kole-m-colorpicker--round |
showValue | true / false | true (由数据驱动,无专属类) false .kole-m-colorpicker--no-value |
代表变体
| 变体 | 标签 |
|---|---|
mode=swatch · round=false · showValue=true | 预设色板(方形色块 + 当前色显示) |
mode=custom · round=false · showValue=true | 自定义色值(色板之后追加色值输入) |
mode=swatch · round=true · showValue=true | 圆点式(主题选择器常用) |
mode=swatch · round=false · showValue=false | 只留色块(空间紧张时) |
用到的令牌
构建时从本组件样式表扫描得出。蓝色为移动端自有令牌,绿色为继承的 PC 令牌(改一处两端生效)。
6 端源码
同一组件的六份实现(生产环境的类名与结构一致,差异只在技术栈写法与单位)。点开查看,右侧可复制。
frameworks-mobile/ColorPicker.css · 纯样式(CSS) · 224 行
/* Kole UI Mobile · ColorPicker 样式 — 对齐移动端规格 §45
颜色选择器:预设色板 + 当前色显示。色板里的色值是**数据**(由宿主传入 presets),
本文件不出现任何 hex —— 每个色块的背景由行内 style 写出,组件样式只负责尺寸、圆角、
选中环与文字色(全部走 --kole-* / --kole-m-* 令牌)。
选中态用「外环 + 放大」表达,不靠色块自身颜色差异(浅色块上看不出高亮)。 */
.kole-m-colorpicker {
/* 组件级变量:业务侧可在容器上覆盖 */
--kole-m-colorpicker-swatch-size: 32px; /* 色块视觉尺寸 */
--kole-m-colorpicker-gap: var(--kole-space-12); /* 色块间距 */
--kole-m-colorpicker-columns: 6; /* 每行色块数 */
--kole-m-colorpicker-radius: var(--kole-radius-base); /* 色块圆角 */
/* 当前色:**由宿主以行内自定义属性传入**(色值是数据,样式表里不出现 hex)。
业务侧通常写 style="--kole-m-colorpicker-current: #RRGGBB",或用 JS setProperty 更新。 */
--kole-m-colorpicker-current: var(--kole-color-border);
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: var(--kole-space-12);
width: 100%;
background: var(--kole-color-card-bg);
color: var(--kole-color-text-body);
font-family: var(--kole-font-family);
font-size: var(--kole-m-font-size-body);
line-height: 1.4;
}
.kole-m-colorpicker__mask {
position: fixed;
inset: 0;
z-index: 2000;
background: var(--kole-color-mask);
opacity: 0;
pointer-events: none;
transition: opacity var(--kole-m-duration-slide) var(--kole-m-ease-slide);
}
.kole-m-colorpicker__mask.is-open {
opacity: 1;
pointer-events: auto;
}
/* 变体 mode=swatch:只有色板,没有任何自定义入口 */
.kole-m-colorpicker--swatch .kole-m-colorpicker__custom { display: none; }
/* 变体 mode=custom:色板之后追加自定义色值输入 */
.kole-m-colorpicker--custom .kole-m-colorpicker__custom { display: flex; }
/* 变体 round=true:色块与当前色圆点都用圆形(与圆点式主题选择器一致) */
.kole-m-colorpicker--round .kole-m-colorpicker__swatch,
.kole-m-colorpicker--round .kole-m-colorpicker__dot { border-radius: 50%; }
/* 变体 showValue=false:不显示色值文字,只留色块(空间紧张时用) */
.kole-m-colorpicker--no-value .kole-m-colorpicker__value { display: none; }
/* 顶部:当前色圆点 + 色值文字 —— 这是「当前色显示」的落点 */
.kole-m-colorpicker__current {
display: flex;
align-items: center;
gap: var(--kole-space-8);
min-height: var(--kole-m-touch-target);
}
.kole-m-colorpicker__dot {
flex: 0 0 auto;
box-sizing: border-box;
width: 24px;
height: 24px;
border: 1px solid var(--kole-color-border);
border-radius: var(--kole-m-colorpicker-radius);
background: var(--kole-m-colorpicker-current);
}
.kole-m-colorpicker__value {
color: var(--kole-color-text-body);
font-family: var(--kole-font-family-num);
font-size: var(--kole-m-font-size-label);
}
.kole-m-colorpicker__caption {
color: var(--kole-color-text-secondary);
font-size: var(--kole-m-font-size-label);
}
/* 色板:定列数网格,色块在各列内居中(375px 与 320px 下都不横向溢出) */
.kole-m-colorpicker__swatches {
display: grid;
grid-template-columns: repeat(var(--kole-m-colorpicker-columns), 1fr);
gap: var(--kole-m-colorpicker-gap);
width: 100%;
}
/* 色块:无文字(颜色本身就是语义,色值由 aria-label 播报),
背景色由宿主以行内自定义属性传入(`--kole-m-colorpicker-swatch`)——
**数据色不进样式表**,组件样式表里没有任何 hex。 */
.kole-m-colorpicker__swatch {
box-sizing: border-box;
justify-self: center;
width: var(--kole-m-colorpicker-swatch-size);
height: var(--kole-m-colorpicker-swatch-size);
padding: 0;
margin: 0;
border: 1px solid var(--kole-color-border);
border-radius: var(--kole-m-colorpicker-radius);
background: var(--kole-m-colorpicker-swatch);
cursor: pointer;
touch-action: manipulation;
transition: transform var(--kole-duration-fast) var(--kole-ease-standard);
}
.kole-m-colorpicker__swatch:active:not(:disabled) { transform: scale(0.94); }
.kole-m-colorpicker__swatch:focus-visible {
outline: 2px solid var(--kole-color-focus-ring);
outline-offset: 2px;
}
/* 状态 selected:外环 + 放大(形状差异,浅色块上也读得出)
外环用 box-shadow 画在色块之外,不占用网格尺寸 */
.kole-m-colorpicker__swatch.is-selected {
transform: scale(1.08);
box-shadow: 0 0 0 2px var(--kole-color-card-bg), 0 0 0 4px var(--kole-color-brand);
}
.kole-m-colorpicker__swatch[aria-checked="true"] {
box-shadow: 0 0 0 2px var(--kole-color-card-bg), 0 0 0 4px var(--kole-color-brand);
}
/* 状态 disabled:整块置灰且不响应 */
.kole-m-colorpicker__swatch:disabled {
cursor: not-allowed;
opacity: 0.4;
}
.kole-m-colorpicker__swatch:disabled:active { transform: none; }
.kole-m-colorpicker.is-disabled .kole-m-colorpicker__value,
.kole-m-colorpicker.is-disabled .kole-m-colorpicker__caption { color: var(--kole-color-text-disabled); }
/* 自定义区(mode=custom):色值输入 + 应用按钮 */
.kole-m-colorpicker__custom {
display: none;
align-items: center;
gap: var(--kole-space-8);
min-height: var(--kole-m-touch-target);
}
.kole-m-colorpicker__input {
flex: 1 1 auto;
min-width: 0;
box-sizing: border-box;
min-height: var(--kole-m-touch-target);
padding: 0 var(--kole-space-12);
border: 1px solid var(--kole-color-border);
border-radius: var(--kole-radius-base);
background: var(--kole-color-card-bg);
color: var(--kole-color-text-body);
font-family: var(--kole-font-family-num);
font-size: var(--kole-m-font-size-label);
}
.kole-m-colorpicker__input:focus-visible {
outline: 2px solid var(--kole-color-focus-ring);
outline-offset: 0;
}
/* 色值非法时置灰提示(不发明「自动纠正」) */
.kole-m-colorpicker__input.is-invalid {
border-color: var(--kole-color-error);
color: var(--kole-color-error);
}
.kole-m-colorpicker__apply {
flex: 0 0 auto;
box-sizing: border-box;
min-height: var(--kole-m-touch-target);
padding: 0 var(--kole-space-16);
border: 1px solid var(--kole-color-brand);
border-radius: var(--kole-radius-base);
background: var(--kole-color-brand);
color: var(--kole-color-text-inverse);
font-family: inherit;
font-size: var(--kole-m-font-size-label);
cursor: pointer;
touch-action: manipulation;
}
.kole-m-colorpicker__apply:focus-visible {
outline: 2px solid var(--kole-color-focus-ring);
outline-offset: 2px;
}
.kole-m-colorpicker__apply:disabled {
border-color: var(--kole-color-border);
background: var(--kole-color-disabled-bg);
color: var(--kole-color-text-disabled);
cursor: not-allowed;
}
.kole-m-colorpicker__error {
color: var(--kole-color-error);
font-size: var(--kole-m-font-size-label);
}
/* 演示页触发器(非组件交付样式) */
.kole-m-colorpicker__trigger {
box-sizing: border-box;
min-height: var(--kole-m-touch-target);
padding: 0 var(--kole-m-gutter);
border: 1px solid var(--kole-color-border);
border-radius: var(--kole-radius-base);
background: var(--kole-color-card-bg);
color: var(--kole-color-text-body);
font-family: inherit;
font-size: var(--kole-m-font-size-label);
cursor: pointer;
touch-action: manipulation;
}
.kole-m-colorpicker__trigger:focus-visible {
outline: 2px solid var(--kole-color-focus-ring);
outline-offset: 2px;
}
frameworks-mobile/ColorPicker.html · H5 原生(无框架) · 230 行
<!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 · ColorPicker(H5)</title>
<link rel="stylesheet" href="../.design_library/kole-ui-mobile/colors_and_type.css">
<link rel="stylesheet" href="ColorPicker.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="swatch">
<p class="demo-label">预设色板(mode=swatch:点色块即选中;选中态是外环 + 放大,浅色块上也读得出)</p>
<div class="demo-pad" data-assert="colorpicker-swatch">
<div class="kole-m-colorpicker kole-m-colorpicker--swatch" id="cp-swatch" data-value="#2F54EB"
style="--kole-m-colorpicker-current: #2F54EB" data-assert="colorpicker-swatch-list">
<div class="kole-m-colorpicker__current">
<span class="kole-m-colorpicker__dot" aria-hidden="true"></span>
<span class="kole-m-colorpicker__value" id="cp-swatch-value">#2F54EB</span>
<span class="kole-m-colorpicker__caption">品牌主色</span>
</div>
<div class="kole-m-colorpicker__swatches" role="radiogroup" aria-label="主题色">
<button class="kole-m-colorpicker__swatch is-selected" type="button" role="radio" aria-checked="true"
data-color="#2F54EB" aria-label="#2F54EB" style="--kole-m-colorpicker-swatch: #2F54EB"></button>
<button class="kole-m-colorpicker__swatch" type="button" role="radio" aria-checked="false"
data-color="#13C2C2" aria-label="#13C2C2" style="--kole-m-colorpicker-swatch: #13C2C2"
data-behavior="click-sets-attr:#cp-swatch|data-value|#13C2C2"></button>
<button class="kole-m-colorpicker__swatch" type="button" role="radio" aria-checked="false"
data-color="#2E7D0A" aria-label="#2E7D0A" style="--kole-m-colorpicker-swatch: #2E7D0A"></button>
<button class="kole-m-colorpicker__swatch" type="button" role="radio" aria-checked="false"
data-color="#8C5A00" aria-label="#8C5A00" style="--kole-m-colorpicker-swatch: #8C5A00"></button>
<button class="kole-m-colorpicker__swatch" type="button" role="radio" aria-checked="false"
data-color="#CF1322" aria-label="#CF1322" style="--kole-m-colorpicker-swatch: #CF1322"></button>
<button class="kole-m-colorpicker__swatch" type="button" role="radio" aria-checked="false"
data-color="#1F2329" aria-label="#1F2329" style="--kole-m-colorpicker-swatch: #1F2329"></button>
</div>
</div>
</div>
</section>
<section class="demo-block" data-demo="value">
<p class="demo-label">当前色显示(色值以等宽字呈现,进读屏的是 aria-label,不靠颜色本身传达信息)</p>
<div class="demo-pad" data-assert="colorpicker-value">
<div class="kole-m-colorpicker kole-m-colorpicker--swatch" id="cp-value"
data-value="#1F2329" style="--kole-m-colorpicker-current: #1F2329">
<div class="kole-m-colorpicker__current">
<span class="kole-m-colorpicker__dot" aria-hidden="true"></span>
<span class="kole-m-colorpicker__value">#1F2329</span>
<span class="kole-m-colorpicker__caption">标题黑</span>
</div>
<div class="kole-m-colorpicker__swatches" role="radiogroup" aria-label="中性色"
style="--kole-m-colorpicker-columns: 4">
<button class="kole-m-colorpicker__swatch" type="button" role="radio" aria-checked="false"
data-color="#CF1322" aria-label="#CF1322" style="--kole-m-colorpicker-swatch: #CF1322"></button>
<button class="kole-m-colorpicker__swatch" type="button" role="radio" aria-checked="false"
data-color="#8C5A00" aria-label="#8C5A00" style="--kole-m-colorpicker-swatch: #8C5A00"></button>
<button class="kole-m-colorpicker__swatch is-selected" type="button" role="radio" aria-checked="true"
data-color="#1F2329" aria-label="#1F2329" style="--kole-m-colorpicker-swatch: #1F2329"></button>
<button class="kole-m-colorpicker__swatch" type="button" role="radio" aria-checked="false"
data-color="#2F54EB" aria-label="#2F54EB" style="--kole-m-colorpicker-swatch: #2F54EB"></button>
</div>
</div>
</div>
</section>
<section class="demo-block" data-demo="custom">
<p class="demo-label">自定义色值(mode=custom:色板之后追加色值输入;非法值置红并给出提示)</p>
<div class="demo-pad" data-assert="colorpicker-custom">
<div class="kole-m-colorpicker kole-m-colorpicker--custom" id="cp-custom" data-value="#2F54EB"
style="--kole-m-colorpicker-current: #2F54EB">
<div class="kole-m-colorpicker__current">
<span class="kole-m-colorpicker__dot" aria-hidden="true"></span>
<span class="kole-m-colorpicker__value" id="cp-custom-value">#2F54EB</span>
<span class="kole-m-colorpicker__caption">当前色</span>
</div>
<div class="kole-m-colorpicker__swatches" role="radiogroup" aria-label="常用色">
<button class="kole-m-colorpicker__swatch is-selected" type="button" role="radio" aria-checked="true"
data-color="#2F54EB" aria-label="#2F54EB" style="--kole-m-colorpicker-swatch: #2F54EB"></button>
<button class="kole-m-colorpicker__swatch" type="button" role="radio" aria-checked="false"
data-color="#13C2C2" aria-label="#13C2C2" style="--kole-m-colorpicker-swatch: #13C2C2"></button>
<button class="kole-m-colorpicker__swatch" type="button" role="radio" aria-checked="false"
data-color="#2E7D0A" aria-label="#2E7D0A" style="--kole-m-colorpicker-swatch: #2E7D0A"></button>
</div>
<div class="kole-m-colorpicker__custom">
<input class="kole-m-colorpicker__input" id="cp-custom-input" type="text" value="#2F54EB"
aria-label="自定义色值" placeholder="#RRGGBB" autocomplete="off" spellcheck="false">
<button class="kole-m-colorpicker__apply" type="button" id="cp-custom-apply">应用</button>
</div>
<p class="kole-m-colorpicker__error" id="cp-custom-error" hidden>色值需为 #RRGGBB 形式</p>
</div>
</div>
</section>
<section class="demo-block" data-demo="round">
<p class="demo-label">圆点式(round=true:色块与当前色圆点都用圆形,主题选择器常用)</p>
<div class="demo-pad" data-assert="colorpicker-round">
<div class="kole-m-colorpicker kole-m-colorpicker--swatch kole-m-colorpicker--round" id="cp-round"
data-value="#13C2C2" style="--kole-m-colorpicker-current: #13C2C2">
<div class="kole-m-colorpicker__current">
<span class="kole-m-colorpicker__dot" aria-hidden="true"></span>
<span class="kole-m-colorpicker__value">#13C2C2</span>
</div>
<div class="kole-m-colorpicker__swatches" role="radiogroup" aria-label="强调色">
<button class="kole-m-colorpicker__swatch" type="button" role="radio" aria-checked="false"
data-color="#2F54EB" aria-label="#2F54EB" style="--kole-m-colorpicker-swatch: #2F54EB"></button>
<button class="kole-m-colorpicker__swatch is-selected" type="button" role="radio" aria-checked="true"
data-color="#13C2C2" aria-label="#13C2C2" style="--kole-m-colorpicker-swatch: #13C2C2"></button>
<button class="kole-m-colorpicker__swatch" type="button" role="radio" aria-checked="false"
data-color="#2E7D0A" aria-label="#2E7D0A" style="--kole-m-colorpicker-swatch: #2E7D0A"></button>
<button class="kole-m-colorpicker__swatch" type="button" role="radio" aria-checked="false"
data-color="#CF1322" aria-label="#CF1322" style="--kole-m-colorpicker-swatch: #CF1322"></button>
</div>
</div>
</div>
</section>
<section class="demo-block" data-demo="disabled">
<p class="demo-label">禁用(状态 disabled:整块置灰、色块不可点;读屏播报不可用)</p>
<div class="demo-pad" data-assert="colorpicker-disabled">
<div class="kole-m-colorpicker kole-m-colorpicker--swatch is-disabled" id="cp-disabled"
data-value="#8C5A00" style="--kole-m-colorpicker-current: #8C5A00">
<div class="kole-m-colorpicker__current">
<span class="kole-m-colorpicker__dot" aria-hidden="true"></span>
<span class="kole-m-colorpicker__value">#8C5A00</span>
<span class="kole-m-colorpicker__caption">已锁定</span>
</div>
<div class="kole-m-colorpicker__swatches" role="radiogroup" aria-label="主题色" aria-disabled="true">
<button class="kole-m-colorpicker__swatch is-selected" type="button" role="radio" aria-checked="true"
data-color="#8C5A00" aria-label="#8C5A00" aria-disabled="true" disabled
style="--kole-m-colorpicker-swatch: #8C5A00"></button>
<button class="kole-m-colorpicker__swatch" type="button" role="radio" aria-checked="false"
data-color="#1F2329" aria-label="#1F2329" aria-disabled="true" disabled
style="--kole-m-colorpicker-swatch: #1F2329"></button>
<button class="kole-m-colorpicker__swatch" type="button" role="radio" aria-checked="false"
data-color="#2F54EB" aria-label="#2F54EB" aria-disabled="true" disabled
style="--kole-m-colorpicker-swatch: #2F54EB"></button>
</div>
</div>
</div>
</section>
</div>
<script>
/* 演示页交互:点色块即选中(同组互斥),当前色圆点与色值文字同步。
色值是**数据**:写在 data-color 与行内自定义属性上,不写进样式表 ——
组件样式表里没有任何 hex(门禁 B3 会扫 CSS)。
每块各自独立:状态只落在本块 DOM 上。 */
(function () {
function applySwatch(root, btn) {
Array.prototype.forEach.call(root.querySelectorAll('.kole-m-colorpicker__swatch'), function (b) {
b.classList.toggle('is-selected', b === btn);
b.setAttribute('aria-checked', b === btn ? 'true' : 'false');
});
var color = btn.getAttribute('data-color') || '';
root.setAttribute('data-value', color);
root.style.setProperty('--kole-m-colorpicker-current', color);
var text = root.querySelector('.kole-m-colorpicker__value');
if (text) text.textContent = color;
}
Array.prototype.forEach.call(document.querySelectorAll('.kole-m-colorpicker'), function (root) {
root.addEventListener('click', function (e) {
var btn = e.target.closest ? e.target.closest('.kole-m-colorpicker__swatch') : null;
if (!btn || !root.contains(btn) || btn.disabled) return;
if (root.classList.contains('is-disabled')) return;
applySwatch(root, btn);
});
});
/* 自定义色值:只接受 #RRGGBB(不发明「自动纠正」,非法值明确报错) */
var input = document.getElementById('cp-custom-input');
var apply = document.getElementById('cp-custom-apply');
var custom = document.getElementById('cp-custom');
var error = document.getElementById('cp-custom-error');
function submitCustom() {
if (!input || !custom) return;
var raw = (input.value || '').trim();
var ok = /^#[0-9a-fA-F]{6}$/.test(raw);
if (error) error.hidden = ok;
input.classList.toggle('is-invalid', !ok);
if (!ok) return;
var upper = raw.toUpperCase();
custom.setAttribute('data-value', upper);
custom.style.setProperty('--kole-m-colorpicker-current', upper);
var text = custom.querySelector('.kole-m-colorpicker__value');
if (text) text.textContent = upper;
Array.prototype.forEach.call(custom.querySelectorAll('.kole-m-colorpicker__swatch'), function (b) {
var on = String(b.getAttribute('data-color')).toUpperCase() === upper;
b.classList.toggle('is-selected', on);
b.setAttribute('aria-checked', on ? 'true' : 'false');
});
}
if (apply) apply.addEventListener('click', submitCustom);
if (input) {
input.addEventListener('keydown', function (e) {
if (e.key === 'Enter') submitCustom();
});
}
})();
</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/ColorPicker.jsx · React · 86 行
import React from 'react';
import './ColorPicker.css';
/* 颜色选择器(移动端)— 规格 §45。
预设色板 + 当前色显示;点色块即回传该色(onChange),同组互斥。
色值是**数据**:写在 data-color 与 `--kole-m-colorpicker-swatch` 行内自定义属性上 ——
组件样式表里不出现任何 hex(浅色/深色主题下色板自身不随主题变,因为颜色就是要选的内容)。
选中态是「外环 + 放大」而不是换色,浅色块上也能读出来(规格 §45.4)。
mode=custom 时追加自定义色值输入(由宿主以 children 传入,外层自备 .kole-m-colorpicker__custom);
本端不做 #RRGGBB 之外的解析 —— 不发明颜色空间转换与自动纠正(规格 §45.7)。 */
/* presets 项可以是字符串,也可以是 { value, label } 对象 */
function colorOf(item, i) {
if (item === null || item === undefined) return '';
if (typeof item === 'object') return String(item.value === undefined ? '' : item.value);
return String(item);
}
function nameOf(item, i) {
if (item && typeof item === 'object' && item.label) return String(item.label);
return colorOf(item, i);
}
export default function ColorPicker({
mode = 'swatch',
round = false,
showValue = true,
disabled = false,
value = '',
presets = [],
columns = 6,
label = '颜色',
onChange,
children = null,
}) {
const current = String(value || '');
const cls =
'kole-m-colorpicker' +
` kole-m-colorpicker--${mode === 'custom' ? 'custom' : 'swatch'}` +
(round ? ' kole-m-colorpicker--round' : '') +
(showValue ? '' : ' kole-m-colorpicker--no-value') +
(disabled ? ' is-disabled' : '');
/* 当前色用行内自定义属性传给样式表(样式表里没有 hex) */
const style = { '--kole-m-colorpicker-current': current || 'transparent' };
const gridStyle = { '--kole-m-colorpicker-columns': String(columns) };
function pick(color) {
if (disabled) return;
if (!onChange) return;
onChange(color);
}
return (
<div className={cls} style={style} aria-label={label} data-value={current}>
<div className="kole-m-colorpicker__current">
<span className="kole-m-colorpicker__dot" aria-hidden="true" />
{showValue ? <span className="kole-m-colorpicker__value">{current || '—'}</span> : null}
</div>
<div className="kole-m-colorpicker__swatches" role="radiogroup" aria-label={label} style={gridStyle}>
{presets.map((item, i) => {
const color = colorOf(item, i);
const on = color.toUpperCase() === current.toUpperCase();
return (
<button
className={'kole-m-colorpicker__swatch' + (on ? ' is-selected' : '')}
key={color + i}
type="button"
role="radio"
aria-checked={on ? 'true' : 'false'}
aria-label={nameOf(item, i)}
aria-disabled={disabled ? 'true' : undefined}
disabled={disabled}
data-color={color}
style={{ '--kole-m-colorpicker-swatch': color }}
onClick={() => pick(color)}
/>
);
})}
</div>
{mode === 'custom' ? <div className="kole-m-colorpicker__custom">{children}</div> : children}
</div>
);
}
frameworks-mobile/ColorPicker.vue2.vue · Vue 2 · 109 行
<template>
<div
class="kole-m-colorpicker"
:class="rootClass"
:style="rootStyle"
:data-value="current"
:aria-label="label"
>
<div class="kole-m-colorpicker__current">
<span class="kole-m-colorpicker__dot" aria-hidden="true"></span>
<span v-if="showValue" class="kole-m-colorpicker__value">{{ current || '—' }}</span>
</div>
<div class="kole-m-colorpicker__swatches" role="radiogroup" :aria-label="label" :style="gridStyle">
<button
v-for="(item, i) in presets"
:key="colorOf(item) + '-' + i"
class="kole-m-colorpicker__swatch"
:class="{ 'is-selected': isOn(item) }"
type="button"
role="radio"
:aria-checked="isOn(item) ? 'true' : 'false'"
:aria-label="nameOf(item)"
:aria-disabled="disabled ? 'true' : null"
:disabled="disabled"
:data-color="colorOf(item)"
:style="{ '--kole-m-colorpicker-swatch': colorOf(item) }"
@click="pick(item)"
></button>
</div>
<div v-if="mode === 'custom'" class="kole-m-colorpicker__custom">
<slot></slot>
</div>
<template v-else>
<slot></slot>
</template>
</div>
</template>
<script>
/* 颜色选择器(移动端)— 规格 §45。
预设色板 + 当前色显示;点色块即回传该色(change),同组互斥。
色值是**数据**:写在 data-color 与 `--kole-m-colorpicker-swatch` 行内自定义属性上 ——
组件样式表里不出现任何 hex。
选中态是「外环 + 放大」而不是换色,浅色块上也能读出来(规格 §45.4)。
mode=custom 时追加自定义色值输入(由宿主以默认插槽传入,外层自备 .kole-m-colorpicker__custom);
本端不做 #RRGGBB 之外的解析 —— 不发明颜色空间转换与自动纠正(规格 §45.7)。 */
/* presets 项可以是字符串,也可以是 { value, label } 对象 */
function colorOf(item) {
if (item === null || item === undefined) return '';
if (typeof item === 'object') return String(item.value === undefined ? '' : item.value);
return String(item);
}
function nameOf(item) {
if (item && typeof item === 'object' && item.label) return String(item.label);
return colorOf(item);
}
export default {
name: 'KoleMColorPicker',
props: {
mode: { type: String, default: 'swatch' },
round: { type: Boolean, default: false },
showValue: { type: Boolean, default: true },
disabled: { type: Boolean, default: false },
value: { type: String, default: '' },
presets: { type: Array, default: function () { return []; } },
columns: { type: Number, default: 6 },
label: { type: String, default: '颜色' }
},
computed: {
current: function () {
return String(this.value || '');
},
rootClass: function () {
return [
'kole-m-colorpicker--' + (this.mode === 'custom' ? 'custom' : 'swatch'),
this.round ? 'kole-m-colorpicker--round' : '',
this.showValue ? '' : 'kole-m-colorpicker--no-value',
this.disabled ? 'is-disabled' : ''
].filter(Boolean);
},
/* 当前色用行内自定义属性传给样式表(样式表里没有 hex) */
rootStyle: function () {
return { '--kole-m-colorpicker-current': this.current || 'transparent' };
},
gridStyle: function () {
return { '--kole-m-colorpicker-columns': String(this.columns) };
}
},
methods: {
colorOf: colorOf,
nameOf: nameOf,
isOn: function (item) {
return colorOf(item).toUpperCase() === this.current.toUpperCase();
},
pick: function (item) {
if (this.disabled) return;
this.$emit('change', colorOf(item));
}
}
};
</script>
<style src="./ColorPicker.css"></style>
frameworks-mobile/ColorPicker.vue3.vue · Vue 3 · 101 行
<template>
<div
class="kole-m-colorpicker"
:class="rootClass"
:style="rootStyle"
:data-value="current"
:aria-label="label"
>
<div class="kole-m-colorpicker__current">
<span class="kole-m-colorpicker__dot" aria-hidden="true"></span>
<span v-if="showValue" class="kole-m-colorpicker__value">{{ current || '—' }}</span>
</div>
<div class="kole-m-colorpicker__swatches" role="radiogroup" :aria-label="label" :style="gridStyle">
<button
v-for="(item, i) in presets"
:key="colorOf(item) + '-' + i"
class="kole-m-colorpicker__swatch"
:class="{ 'is-selected': isOn(item) }"
type="button"
role="radio"
:aria-checked="isOn(item) ? 'true' : 'false'"
:aria-label="nameOf(item)"
:aria-disabled="disabled ? 'true' : null"
:disabled="disabled"
:data-color="colorOf(item)"
:style="{ '--kole-m-colorpicker-swatch': colorOf(item) }"
@click="pick(item)"
></button>
</div>
<div v-if="mode === 'custom'" class="kole-m-colorpicker__custom">
<slot></slot>
</div>
<template v-else>
<slot></slot>
</template>
</div>
</template>
<script setup>
/* 颜色选择器(移动端)— 规格 §45。
预设色板 + 当前色显示;点色块即回传该色(change),同组互斥。
色值是**数据**:写在 data-color 与 `--kole-m-colorpicker-swatch` 行内自定义属性上 ——
组件样式表里不出现任何 hex。
选中态是「外环 + 放大」而不是换色,浅色块上也能读出来(规格 §45.4)。
mode=custom 时追加自定义色值输入(由宿主以默认插槽传入,外层自备 .kole-m-colorpicker__custom);
本端不做 #RRGGBB 之外的解析 —— 不发明颜色空间转换与自动纠正(规格 §45.7)。 */
import { computed } from 'vue';
const props = defineProps({
mode: { type: String, default: 'swatch' },
round: { type: Boolean, default: false },
showValue: { type: Boolean, default: true },
disabled: { type: Boolean, default: false },
value: { type: String, default: '' },
presets: { type: Array, default: () => [] },
columns: { type: Number, default: 6 },
label: { type: String, default: '颜色' }
});
const emit = defineEmits(['change']);
/* presets 项可以是字符串,也可以是 { value, label } 对象 */
function colorOf(item) {
if (item === null || item === undefined) return '';
if (typeof item === 'object') return String(item.value === undefined ? '' : item.value);
return String(item);
}
function nameOf(item) {
if (item && typeof item === 'object' && item.label) return String(item.label);
return colorOf(item);
}
const current = computed(() => String(props.value || ''));
const rootClass = computed(() => [
'kole-m-colorpicker--' + (props.mode === 'custom' ? 'custom' : 'swatch'),
props.round ? 'kole-m-colorpicker--round' : '',
props.showValue ? '' : 'kole-m-colorpicker--no-value',
props.disabled ? 'is-disabled' : ''
].filter(Boolean));
/* 当前色用行内自定义属性传给样式表(样式表里没有 hex) */
const rootStyle = computed(() => ({
'--kole-m-colorpicker-current': current.value || 'transparent'
}));
const gridStyle = computed(() => ({ '--kole-m-colorpicker-columns': String(props.columns) }));
function isOn(item) {
return colorOf(item).toUpperCase() === current.value.toUpperCase();
}
function pick(item) {
if (props.disabled) return;
emit('change', colorOf(item));
}
</script>
<style src="./ColorPicker.css"></style>
frameworks-mobile/ColorPicker.uniapp.vue · uni-app(跨端:小程序 / App / H5) · 188 行
<template>
<view
class="kole-m-colorpicker"
:class="rootClass"
:style="rootStyle"
:data-value="current"
:aria-label="label"
>
<view class="kole-m-colorpicker__current">
<view class="kole-m-colorpicker__dot" aria-hidden="true"></view>
<text v-if="showValue" class="kole-m-colorpicker__value">{{ current || '—' }}</text>
</view>
<view class="kole-m-colorpicker__swatches" :role="'radiogroup'" :aria-label="label" :style="gridStyle">
<view
v-for="(item, i) in presets"
:key="colorOf(item) + '-' + i"
class="kole-m-colorpicker__swatch"
:class="isOn(item) ? 'is-selected' : ''"
:role="disabled ? '' : 'radio'"
:aria-checked="isOn(item) ? 'true' : 'false'"
:aria-label="nameOf(item)"
:aria-disabled="disabled ? 'true' : 'false'"
:data-color="colorOf(item)"
:style="{ '--kole-m-colorpicker-swatch': colorOf(item) }"
@tap="pick(item)"
></view>
</view>
<view v-if="mode === 'custom'" class="kole-m-colorpicker__custom">
<slot></slot>
</view>
<slot v-else></slot>
</view>
</template>
<script setup>
/* uni-app 端 · 颜色选择器(移动端)— 规格 §45
跨端差异:用 view / text;色块是 view + role="radio"(小程序没有可聚焦的原生 button 语义),
点击用 @tap;禁用不用原生 disabled(view 没有),改为 aria-disabled + 代码拦截。
色值是**数据**:写在 data-color 与 `--kole-m-colorpicker-swatch` 行内自定义属性上 ——
组件样式表里不出现任何 hex(颜色本身就是要选的内容,不随主题变)。
选中态是「外环 + 放大」而不是换色,浅色块上也能读出来(规格 §45.4)。
尺寸用 rpx(88rpx = 375pt 下的 44px 触控最小边长,色块 64rpx = 32px)。 */
import { computed } from 'vue';
const props = defineProps({
mode: { type: String, default: 'swatch' },
round: { type: Boolean, default: false },
showValue: { type: Boolean, default: true },
disabled: { type: Boolean, default: false },
value: { type: String, default: '' },
presets: { type: Array, default: () => [] },
columns: { type: Number, default: 6 },
label: { type: String, default: '颜色' }
});
const emit = defineEmits(['change']);
/* presets 项可以是字符串,也可以是 { value, label } 对象 */
function colorOf(item) {
if (item === null || item === undefined) return '';
if (typeof item === 'object') return String(item.value === undefined ? '' : item.value);
return String(item);
}
function nameOf(item) {
if (item && typeof item === 'object' && item.label) return String(item.label);
return colorOf(item);
}
const current = computed(() => String(props.value || ''));
const rootClass = computed(() => [
'kole-m-colorpicker--' + (props.mode === 'custom' ? 'custom' : 'swatch'),
props.round ? 'kole-m-colorpicker--round' : '',
props.showValue ? '' : 'kole-m-colorpicker--no-value',
props.disabled ? 'is-disabled' : ''
].filter(Boolean));
/* 当前色用行内自定义属性传给样式表(样式表里没有 hex) */
const rootStyle = computed(() => ({
'--kole-m-colorpicker-current': current.value || 'transparent'
}));
const gridStyle = computed(() => ({ '--kole-m-colorpicker-columns': String(props.columns) }));
function isOn(item) {
return colorOf(item).toUpperCase() === current.value.toUpperCase();
}
function pick(item) {
if (props.disabled) return;
emit('change', colorOf(item));
}
</script>
<style>
.kole-m-colorpicker {
--kole-m-colorpicker-swatch-size: 64rpx;
--kole-m-colorpicker-gap: 24rpx;
--kole-m-colorpicker-columns: 6;
--kole-m-colorpicker-current: var(--kole-color-border);
--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);
}
/* 变体 mode=swatch:只有色板,没有任何自定义入口 */
.kole-m-colorpicker--swatch .kole-m-colorpicker__custom { display: none; }
/* 变体 mode=custom:色板之后追加自定义色值输入 */
.kole-m-colorpicker--custom .kole-m-colorpicker__custom {
display: flex;
align-items: center;
}
/* 变体 round=true:色块与当前色圆点都用圆形 */
.kole-m-colorpicker--round .kole-m-colorpicker__swatch,
.kole-m-colorpicker--round .kole-m-colorpicker__dot { border-radius: 50%; }
/* 变体 showValue=false:不显示色值文字,只留色块 */
.kole-m-colorpicker--no-value .kole-m-colorpicker__value { display: none; }
/* 顶部:当前色圆点 + 色值文字 —— 这是「当前色显示」的落点 */
.kole-m-colorpicker__current {
display: flex;
align-items: center;
box-sizing: border-box;
min-height: var(--kole-m-touch-target);
}
.kole-m-colorpicker__dot {
box-sizing: border-box;
width: 48rpx;
height: 48rpx;
margin-right: 16rpx;
border: 1rpx solid var(--kole-color-border);
border-radius: 8rpx;
background-color: var(--kole-m-colorpicker-current);
}
.kole-m-colorpicker__value {
color: var(--kole-color-text-body);
font-size: var(--kole-m-font-size-label);
}
/* 色板:定列数网格(flex + calc 百分比实现,小程序对 grid 支持不一),色块在各列内居中 */
.kole-m-colorpicker__swatches {
display: flex;
flex-wrap: wrap;
width: 100%;
}
.kole-m-colorpicker__swatch {
box-sizing: border-box;
width: calc(100% / var(--kole-m-colorpicker-columns));
height: var(--kole-m-colorpicker-swatch-size);
margin: 0;
border-radius: 8rpx;
background-color: var(--kole-m-colorpicker-swatch);
}
/* 状态 selected:外环 + 放大(形状差异,浅色块上也读得出) */
.kole-m-colorpicker__swatch.is-selected {
transform: scale(1.08);
border: 4rpx solid var(--kole-color-brand);
}
/* 状态 disabled:整块置灰且不响应 */
.kole-m-colorpicker__swatch[aria-disabled='true'] { opacity: 0.4; }
.kole-m-colorpicker.is-disabled .kole-m-colorpicker__value { color: var(--kole-color-text-disabled); }
/* 自定义区(mode=custom):色值输入 + 应用按钮,由宿主以插槽填入 */
.kole-m-colorpicker__custom {
display: none;
box-sizing: border-box;
min-height: var(--kole-m-touch-target);
}
</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-colorpicker.json(点击展开原始 JSON)
{
"schemaVersion": 1,
"sourceKind": "authored-spec",
"provenance": "authored-in-repo",
"specFile": "spec/移动端规格.md",
"specSection": "45 · 颜色选择器 ColorPicker",
"confidence": "high",
"slug": "mobile-colorpicker",
"name": "颜色选择器 ColorPicker",
"semanticTypeCandidates": [
"colorpicker",
"swatch",
"palette"
],
"variantDimensions": [
{
"name": "mode",
"values": [
"swatch",
"custom"
]
},
{
"name": "round",
"values": [
"false",
"true"
]
},
{
"name": "showValue",
"values": [
"true",
"false"
]
}
],
"representativeVariants": [
{
"mode": "swatch",
"round": "false",
"showValue": "true",
"label": "预设色板(方形色块 + 当前色显示)"
},
{
"mode": "custom",
"round": "false",
"showValue": "true",
"label": "自定义色值(色板之后追加色值输入)"
},
{
"mode": "swatch",
"round": "true",
"showValue": "true",
"label": "圆点式(主题选择器常用)"
},
{
"mode": "swatch",
"round": "false",
"showValue": "false",
"label": "只留色块(空间紧张时)"
}
],
"anatomy": {
"colorpicker": "根元素,承载当前色与色板",
"colorpicker__current": "当前色显示,由「圆点 + 色值文字」组成",
"colorpicker__dot": "当前色圆点,背景取 --kole-m-colorpicker-current(色值由宿主以行内自定义属性传入)",
"colorpicker__value": "色值文字(等宽字),读屏靠 aria-label 而不是色块本身",
"colorpicker__swatches": "色板,role=radiogroup + aria-label,定列数网格",
"colorpicker__swatch": "单个色块,role=radio + aria-checked,背景取 --kole-m-colorpicker-swatch",
"colorpicker__custom": "自定义色值输入区(仅 mode=custom),由宿主以插槽填入"
},
"structurePatterns": {
"mode": "swatch(只有预设色板)/ custom(色板之后追加自定义色值输入)",
"round": "false(方形色块)/ true(圆形,主题选择器常用)",
"showValue": "true(显示色值文字)/ false(只留色块,空间紧张时用)",
"columns": "色板每行列数(默认 6,通过 --kole-m-colorpicker-columns 覆盖)"
},
"usageHints": [
"从一组预设色里挑一个颜色(主题色、标签色、看板分类色),或在预设之外补一个自定义色值",
"移动端默认只给预设色板:色值离散可预期;需要自由取色时退化为「输入色值」而不是「拖色相」",
"点色块即选中并回传该色;同组互斥(选新的自动取消旧的)",
"选中反馈是外环 + 轻微放大,不是换色 —— 浅色块(白、浅黄)上换色根本看不出来",
"mode=custom 时色值输入只在确认(点「应用」或回车)后生效,输入过程中不回传",
"色值非法时输入框置红并给出错误文案,不自动纠正"
],
"doNotInvent": [
"色相环 / 明度滑条的取色画布(移动端精度不足,本组件不提供)",
"颜色空间转换(RGB / HSL / HSV 互转)与色值自动纠正(只接受 #RRGGBB)",
"主题派生(由品牌色自动生成 hover / active / 浅底等衍生色阶)",
"取色器(吸管)与屏幕取色"
],
"unknowns": [
"色板上限(当前不限制数量,但 40 个以上在小屏上会变成一片色噪)",
"自定义色值的格式是否要支持 rgb() / hsl() 书写",
"是否需要「最近使用」一行动态色块"
],
"interaction": [
"点色块即选中并回传该色;同组互斥(选新的自动取消旧的)",
"色块视觉 ≥32px、热区按网格列宽铺满,点空白处不响应",
"选中反馈是外环 + scale(1.08),变换用 --kole-duration-fast;减少动态偏好下不放大",
"mode=custom 时色值输入只在确认(点「应用」或回车)后生效,输入过程中不回传",
"色值非法时输入框置红并给出错误文案,不自动纠正(不发明「就近取色」这类行为)"
],
"accessibility": [
"色板 role=\"radiogroup\" + aria-label 说明这组在选什么(如「主题色」)",
"每个色块 role=\"radio\" + aria-checked,aria-label 必须是色值本身(如 #2F54EB)—— 色块没有文字,读屏只能靠 aria-label",
"当前色以等宽文字重复一遍:颜色不作为唯一信息通道(色觉障碍用户读得到色值)",
"禁用块写 aria-disabled=\"true\",读屏播报不可用"
],
"api": {
"source": "implementation",
"note": "props / events / slots 为 6 端实现的公共接口(说明文字取自规格对应小节)。字段名与各端源码逐名核对:node tools/verify-mobile-docs.mjs",
"requiredNote": "「必传」按严格定义:实现里**没有默认值**时才为 Y(本门禁逐条核对 props 与各端源码的默认值,防止契约与实现脱节)。",
"props": [
{
"name": "mode",
"type": "'swatch' | 'custom'",
"default": "'swatch'",
"desc": "变体 mode:swatch 只有预设色板,custom 追加自定义色值输入(规格 §45.3)",
"required": false
},
{
"name": "round",
"type": "boolean",
"default": "false",
"desc": "变体 round:色块与当前色圆点是否用圆形(规格 §45.3)",
"required": false
},
{
"name": "showValue",
"type": "boolean",
"default": "true",
"desc": "变体 showValue:是否显示色值文字(规格 §45.3)",
"required": false
},
{
"name": "disabled",
"type": "boolean",
"default": "false",
"desc": "状态 disabled:整块置灰且不响应(规格 §45.4)",
"required": false
},
{
"name": "value",
"type": "string",
"default": "''",
"desc": "当前色值,#RRGGBB(规格 §45.6)",
"required": false
},
{
"name": "presets",
"type": "Array",
"default": "[]",
"desc": "预设色板(宿主提供,本组件不发明配色方案)(规格 §45.7)",
"required": false
},
{
"name": "columns",
"type": "number",
"default": "6",
"desc": "色板每行列数,映射到 --kole-m-colorpicker-columns(规格 §45.3)",
"required": false
},
{
"name": "label",
"type": "string",
"default": "'颜色'",
"desc": "色板 aria-label 与 radiogroup 名称,读屏靠它知道这组在选什么(规格 §45.6)",
"required": false
}
],
"events": [
{
"name": "change",
"params": "value: string",
"desc": "点色块选中时触发,回传色值 #RRGGBB(规格 §45.5)"
}
],
"slots": [
{
"name": "default",
"desc": "自定义内容;mode=custom 时放进 .kole-m-colorpicker__custom(色值输入 + 应用按钮)"
}
]
},
"variantClasses": {
"mode": {
"swatch": [
".kole-m-colorpicker--swatch"
],
"custom": [
".kole-m-colorpicker--custom"
]
},
"round": {
"false": [],
"true": [
".kole-m-colorpicker--round"
]
},
"showValue": {
"true": [],
"false": [
".kole-m-colorpicker--no-value"
]
}
},
"demos": [
{
"id": "swatch",
"group": "01 组件类型",
"title": "预设色板",
"desc": "mode=swatch:点色块即选中,选中态是外环 + 放大,浅色块上也读得出。",
"variant": "mode=swatch"
},
{
"id": "value",
"group": "02 组件状态",
"title": "当前色显示",
"desc": "色值以等宽字呈现,颜色不作为唯一信息通道:读屏与色觉障碍用户读得到色值。",
"variant": "状态 selected"
},
{
"id": "custom",
"group": "01 组件类型",
"title": "自定义色值",
"desc": "mode=custom:色板之后追加色值输入,非法值置红并给出提示,不自动纠正。",
"variant": "mode=custom"
},
{
"id": "round",
"group": "01 组件类型",
"title": "圆点式",
"desc": "round=true:色块与当前色圆点都用圆形,主题选择器常用。",
"variant": "round=true"
},
{
"id": "disabled",
"group": "02 组件状态",
"title": "禁用",
"desc": "整块置灰且色块不可点,读屏播报不可用。",
"variant": "状态 disabled"
}
],
"related": [
{
"slug": "mobile-tag",
"why": "标签只需要展示颜色(由宿主给背景色),不需要用户去选,用标签而不是颜色选择器"
},
{
"slug": "mobile-popup",
"why": "色板要放进浮层里时用它承载,颜色选择器本身不管浮层"
},
{
"slug": "mobile-picker",
"why": "选项是有名字的枚举(主题名:暗色 / 亮色 / 跟随系统)时用选择器,颜色只是它的呈现"
},
{
"slug": "mobile-input",
"why": "需要用户手写 rgb() / hsl() 这类非十六进制写法时的兜底输入"
},
{
"slug": "mobile-list",
"why": "颜色以「名称 + 色块」成行罗列时用列表承载,点整行即选"
}
]
}