移动端导航颜色选择器

颜色选择器ColorPicker

从一组预设色里挑一个颜色(主题色、标签色、看板分类色),或在预设之外补一个自定义色值

数据录入 规格 45 · 颜色选择器 ColorPicker 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-colorpicker.css">

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

演示

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

01 组件类型

预设色板mode=swatch

mode=swatch:点色块即选中,选中态是外环 + 放大,浅色块上也读得出。

查看代码(演示页原文 · 28 行)
frameworks-mobile/ColorPicker.html · swatch
<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

mode=custom:色板之后追加色值输入,非法值置红并给出提示,不自动纠正。

查看代码(演示页原文 · 27 行)
frameworks-mobile/ColorPicker.html · custom
<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

round=true:色块与当前色圆点都用圆形,主题选择器常用。

查看代码(演示页原文 · 22 行)
frameworks-mobile/ColorPicker.html · round
<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 组件状态

当前色显示状态 selected

色值以等宽字呈现,颜色不作为唯一信息通道:读屏与色觉障碍用户读得到色值。

查看代码(演示页原文 · 24 行)
frameworks-mobile/ColorPicker.html · value
<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>
禁用状态 disabled

整块置灰且色块不可点,读屏播报不可用。

查看代码(演示页原文 · 24 行)
frameworks-mobile/ColorPicker.html · disabled
<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
roundbooleanfalse变体 round:色块与当前色圆点是否用圆形(规格 §45.3)N
showValuebooleantrue变体 showValue:是否显示色值文字(规格 §45.3)N
disabledbooleanfalse状态 disabled:整块置灰且不响应(规格 §45.4)N
valuestring''当前色值,#RRGGBB(规格 §45.6)N
presetsArray<string | { value: string, label?: string }>[]预设色板(宿主提供,本组件不发明配色方案)(规格 §45.7)N
columnsnumber6色板每行列数,映射到 --kole-m-colorpicker-columns(规格 §45.3)N
labelstring'颜色'色板 aria-label 与 radiogroup 名称,读屏靠它知道这组在选什么(规格 §45.6)N

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

事件

名称参数说明
changevalue: string点色块选中时触发,回传色值 #RRGGBB(规格 §45.5)

插槽

名称说明
default自定义内容;mode=custom 时放进 .kole-m-colorpicker__custom(色值输入 + 应用按钮)

CSS 变量

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

名称默认值说明
--kole-m-colorpicker-swatch-size32px色块视觉尺寸
--kole-m-colorpicker-gapvar(--kole-space-12)色块间距
--kole-m-colorpicker-columns6每行色块数
--kole-m-colorpicker-radiusvar(--kole-radius-base)色块圆角
--kole-m-colorpicker-currentvar(--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 校验(类/变量必须真实存在)。

维度取值对应类名 / 变量
modeswatch / custom
swatch .kole-m-colorpicker--swatch
custom .kole-m-colorpicker--custom
roundfalse / true
false (由数据驱动,无专属类)
true .kole-m-colorpicker--round
showValuetrue / 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 令牌(改一处两端生效)。

--kole-m-duration-slide --kole-m-ease-slide --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-card-bg --kole-color-disabled-bg --kole-color-error --kole-color-focus-ring --kole-color-mask --kole-color-text-body --kole-color-text-disabled --kole-color-text-inverse --kole-color-text-secondary --kole-duration-fast --kole-ease-standard --kole-font-family --kole-font-family-num --kole-radius-base --kole-space-12 --kole-space-16 --kole-space-8 --kole-m-colorpicker-columns --kole-m-colorpicker-current --kole-m-colorpicker-gap --kole-m-colorpicker-radius --kole-m-colorpicker-swatch --kole-m-colorpicker-swatch-size

6 端源码

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

frameworks-mobile/ColorPicker.css · 纯样式(CSS) · 224 行
frameworks-mobile/ColorPicker.css
/* 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 行
frameworks-mobile/ColorPicker.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 · 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 行
frameworks-mobile/ColorPicker.jsx
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 行
frameworks-mobile/ColorPicker.vue2.vue
<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 行
frameworks-mobile/ColorPicker.vue3.vue
<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 行
frameworks-mobile/ColorPicker.uniapp.vue
<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": "颜色以「名称 + 色块」成行罗列时用列表承载,点整行即选"
    }
  ]
}