组件

控件建立在四档高度上——24、28、32、40。32 是按钮的默认档,40 是输入框族的默认档。

每个可交互元素支持同一套状态,且这套是封闭的:default、hover、active、focus-visible、disabled、loading、selected、error。没有别的状态。一个需要新状态的控件,是一个需要重新想清楚的控件。

按钮

破坏性操作用次级外壳配 error 文字,并加二次确认——绝不用红色实底。一个大红「删除」恰恰招来它本要防止的误点。按钮文案是动词:生成、导出、重新生成,不是「确定」不是「提交」。loading 保留宽度和文案,只把前置图标换成 spinner 并设 aria-busy

输入框族:白底 + 描边

宽度需在 40–900 mm 之间。

输入框族一律白底 + 1px stroke-input 描边。白底加描边把"可以往里写字"说得最清楚;灰底 background-input 因此空出来,专门留给取值胶囊和分段轨道。状态全靠描边表达:hover 提到 stroke-strong,focus 提到 brand-default,error 取 error。错误信息放在字段旁边。

取值胶囊:一种外形,四种宽度

%
mm

统一落在外形上,宽度随内容走。四个胶囊的填充、描边、圆角、高度、对齐、字重完全一致; 宽度按内容分档。视觉锚点是右边缘,而它由布局对齐着——所以左边缘参差不齐不影响读取, 而一位数躺在三位数的框里会很显眼。每档余量控制在 13px 以内。

构成用于
chip-w-sm48px10 + 两位数 17 + 10纯整数,无单位
chip-w64px10 + 17 + 单位 26–34两位数 + % 或 mm
chip-w-lg80px10 + 三位数 25 + 共用格 32带拖拽手柄
自定88px+按位数算六位以上(如种子)

统一的是外形,不是宽度。外形——白底、stroke-input 描边、8px 圆角、32 高、右对齐、500 字重、tabular-nums——必须完全一致;宽度按内容分档,每档余量控制在 13px 以内。

数值字段的横向拖拽

原生上下箭头一律不用:命中区小,和工具面板的语域不符。改成右端一个 24×24 的横向拖拽手柄。把光标移到下面的胶囊上试试——单位淡出、手柄淡入。

mm

单位后缀和拖拽手柄占同一格。各留一格会在每个胶囊右侧多出 24px 常年空白(手柄不 hover 时看不见)。你正要拖的时候不需要看单位。 其余规则:位移 < 3px 判定为点击 → 聚焦并全选 · Shift 十倍步进 · setPointerCapture 拖出面板不丢 · RTL 下向左拖是增大 · 拿掉原生 spinner 就丢了键盘粗调,必须补 Shift + ↑/↓ = 十步 · 手柄 aria-hidden 且不进 Tab 序列 · 触摸设备手柄常驻 · 同一行已有滑块时不加手柄

下拉:自造 listbox

经典榫卯
宽榫头
波浪

原生 <select> 的弹出层无法换肤,在工具面板里会突然掉出一块系统 UI。所以自造,代价是把整份键盘与屏幕阅读器契约还回来,这不是可选项。选中态是 background-active 加一个对勾——光靠底色区分选中,在只有四项的列表里太弱,而且颜色不能单独承载含义。

契约项要求
角色role="combobox" / listbox / option,配 aria-expandedaria-selectedaria-activedescendant
焦点模型ARIA 1.2 select-only combobox:焦点始终留在触发器上,不做焦点搬运(搬焦点更容易在关闭时把焦点丢在 body 上)
关闭态键位↑↓ / Enter / Space / Home / End / 任意字符 → 展开
展开态键位↑↓ 移动 · Home/End 到头尾 · Enter/Space 选中并关闭 · Escape 关闭且不改值 · Tab 关闭 · 点击外部关闭 · 首字母跳转
焦点归还关闭后把焦点还给触发器
浮层挂载挂到 bodyposition:fixed——留在原地会被滚动容器和 overflow:hidden 裁掉。空间不够向上翻,夹在视口内。RTL 下与触发器边缘对齐
无脚本回退HTML 里保留原生 <select>,样式与触发器一致
移动端代价丢掉系统选择器,32px 选项在触屏偏小。手机场景多的话,窄断点下退回原生

滑块:三个数字是承重的

三个数字是承重的:轨道 6px、未填充段用 track-offneutral-300)、 手柄 18px 配 1px stroke-strong 描边和 knob 阴影。 未填充段的 1.47:1 靠面积成立——同样的比值画成发丝线就看不见了,画成 6px 带子就看得见; 已填充与未填充之间是 12:1,位置一眼读出。手柄的描边同理:白圆落在浅轨道上,光靠阴影会化开, 而手柄是靠形状承载数值位置的东西。

滑块是参数驱动型生成器的主力,而且从不单独使用。当前值永远以数字形式显示在行尾——手柄位置不是一个数值。当精度要紧时(步数、种子、尺寸)给它配数字输入框,因为滑块没法打字。步长和范围来自模型的真实约束,而不是来自"手感顺滑";被静默截断的值是用户看不见的 bug。禁用态要看得出"在这儿但不能动",而不是整条消失。

开关

结构是一个真实的 <input type="checkbox">,轨道和滑块只是画在它之上的装饰每一层装饰都必须 pointer-events: none,点击要落到底下那个真实 input 上; 并把整个东西包进它的 <label>,让命中区覆盖文字。轨道高 24px 刚好等于最小命中区。

分段控件 与 分区标题

分段控件:灰轨道 + 3px 内边距 + 10px 圆角,活动段白色 + micro。它不是 Tab,不承载页面切换。 分区标题:整行是一个 <button aria-expanded>,标题文字在左、箭头在行尾——这样标题文字和字段名对齐成一条线,箭头和取值胶囊对齐成另一条线。箭头用上/下而不是左/右,因为上下不参与 RTL 镜像,省掉一条特例规则。只动 hidden 和一个 transform,没有高度动画。

说明文字要吝啬

%
%
实际割缝由功率和焦距决定。

说明文字要吝啬。能写进单位后缀的就不要写成一句话——20 % 好过"占单块边长的百分比"; 能从结果栏读到的就不要在配置栏重复。三级文本色适合一行提示,不适合成段。 理想状态是整个配置面板只剩两三条真正带约束的说明,比如上面这条——它给的是生产事实,不是把标签再说一遍。

Esc
输入关键词搜索全部文档 · 切换 · 打开