button · 七态矩阵

按钮取七态,不是九态。另外两态(empty / error)属于容器,按钮没有"空"的概念。

实测动机:某项目 的按钮只有 default + hover 两态,:disabled 0 处。 用户不会抱怨"你缺 disabled 态",只会觉得"这个按钮点了没反应,是不是坏了"。

七态

1. default

静止。accent 底 + 白字,实测 5.87 AA。

2. hover

明确的 --accent-hover 色值(白字 7.73 AAA),不是 brightness 计算。

3. active

下沉 1px。只用 1px——用 4px/6px 会有果冻感,那是廉价感的主要来源。

4. focus-visible

键盘 Tab 到这里才出现。:focus 会让鼠标点击也冒框,所以用 -visible。

5. disabled

明确色值,不用 opacity——opacity 让对比度变成不可控的随机数。hover 无任何变化。

6. loading

宽度不变(label 与 spinner 同一 grid 单元,容器宽 = max(两者))。pointer-events:none 防重复提交。

7. success

仅用于不可逆动作(提交、支付)。普通操作加这个是噪音。

交互演示

点一下:default → loading(1.4s)→ success(1.2s)→ 复位。注意宽度全程不变。

变体

一个视图里只能有一个 primary。danger 不应该做得和 primary 一样显眼。

尺寸

md = 44px 是 WCAG 2.5.5 增强级的触摸目标尺寸,来自手指物理尺寸,不是审美。 sm 只在表格行内等鼠标场景用。

两种形态:方形图标按钮 / 占满一行

.btn--icon · 只有图标

宽高相等(--md 44、--lg 52、--sm 32),所以图标必须是唯一内容。 ⇒ 读屏读不到文字,必须显式给 aria-label, 否则念出来只有"按钮"两个字。

.btn--icon.btn--sm 是 32×32:只够 WCAG 2.2 的 AA(24px), 不满足 44px 的 AAA ⇒ 所以这里故意不演示它。 它只该出现在表格行、标签条这类鼠标为主、相邻控件本身就很密的地方; 触屏主流程请用 md。契约第 ③ 条要求本页所有可点元素 ≥44px, 放一颗 32px 的按钮进来就会红 —— 那个红是对的。

.btn--block · 占满容器

不是 width:100% 的按钮,而是 display:flex 的容器 ⇒ 内容自动居中,多个子元素(图标 + 文字)也不会各靠一边。

外面这个 280px 的框就是它的容器:按钮正好铺满,不多不少。 宽度来自容器而不是按钮自己 ⇒ 放进窄侧栏、弹窗底部都不会溢。

对比:常见的错写法 vs 本库写法

常见写法(某项目 现状)

filter: brightness(1.08) —— hover 色是算出来的,无法验证对比度,不同底色表现不一致。
transform: scale(1.05) —— 按钮"长大",廉价感。
opacity: 0.5 —— 对比度变成随机数。
transition: all —— 所有属性一起动画,性能和观感失控。

本库写法

明确的 --accent-hover 色值,实测 7.73 AAA。
active 只下沉 1px。
disabled 用明确的低对比色组,仍可读。
逐属性写 transition,只动颜色和 transform。

一个容易被跳过的判断:disabled 还是 aria-disabled

disabled 属性

从 Tab 顺序移除,键盘用户永远找不到它。适合永久不可用(权限不足)。

aria-disabled="true"

保留可聚焦,屏幕阅读器读"不可用"。表单提交按钮应该用这个——用户 Tab 到它才知道"还差点什么"。

spinner 与 prefers-reduced-motion

系统开启"减少动效"后,全局兜底会把动画压到 0.01ms——spinner 就停了,用户会以为页面卡死。

WCAG 的 reduced-motion 针对的是大幅位移(前庭功能障碍), 小幅原地旋转不属于此类。本库的做法是放慢到 1.2s 但保留,不停。

这是"照抄规范"和"理解规范"的区别。