用途
本技能用于指导按钮及各类可交互元素的状态设计。它适用于设计按钮、链接、输入框或任何可点击组件,确保每个交互组件在每种交互模式下都有完整且视觉可区分的状态。缺失或含糊的状态会让界面显得未完成,并降低用户信心。
六种状态
每个交互组件必须覆盖以下状态:
| 状态 | 触发方式 | 视觉信号 |
|---|---|---|
| 静止(Rest) | 默认 | 基础色,cursor: pointer |
| 悬停(Hover) | 鼠标悬停 | 略微加深,背景轻微变化 |
| 按下(Active / Pressed) | 鼠标按下 / 轻触 | 明显加深,轻微缩小 |
| 聚焦(Focus) | 键盘导航 | 可见的聚焦环,不改变填充 |
| 禁用(Disabled) | 不可用 | 低对比度,cursor: not-allowed,无交互 |
| 加载(Loading) | 异步操作进行中 | 旋转指示器或脉冲,不可交互 |
算法化派生状态颜色
状态颜色不是独立选取的,而是从基础色出发,通过调整 HSL 亮度派生,以保证整套色板的一致性:
base: hsl(H, S%, L%)
hover: hsl(H, S%, L% - 8%) ← 加深 8%
active: hsl(H, S%, L% - 14%) ← 加深 14%
示例:主按钮 #635BFF(hsl 243, 100%, 68%)
.btn-primary {
background: hsl(243, 100%, 68%); /* rest #635BFF */
}
.btn-primary:hover {
background: hsl(243, 100%, 60%); /* hover #4A40FF */
}
.btn-primary:active {
background: hsl(243, 100%, 54%); /* active #3429FF */
}
对于深色背景上的浅色按钮,逻辑反转——悬停时变浅而非加深。
次要 / 描边按钮
.btn-secondary {
background: transparent;
border: 1px solid var(--color-border);
color: var(--color-text);
}
.btn-secondary:hover {
background: var(--color-grey-100); /* subtle fill */
border-color: var(--color-grey-300);
}
.btn-secondary:active {
background: var(--color-grey-200);
}
聚焦状态
聚焦是键盘导航的要求(WCAG 2.2)。它必须可见,且不能仅依赖悬停样式——键盘用户不会触发悬停。
.btn:focus-visible {
outline: 2px solid var(--color-primary);
outline-offset: 3px;
}
要点:
- 聚焦环使用 outline 而非 box-shadow——outline 在现代浏览器中尊重 border-radius,且不影响布局。
- outline-offset: 2–4px 让聚焦环与组件边缘有呼吸空间。
- 绝不在没有替代聚焦样式的情况下使用 outline: none。
禁用状态
.btn:disabled,
.btn[aria-disabled="true"] {
opacity: 0.4;
cursor: not-allowed;
pointer-events: none;
}
要点:
- 禁用元素不受 WCAG 对比度要求约束——低透明度是正确的、有意的。
- 使用 pointer-events: none 即使 JS 被绕过也能阻止点击事件。
- 不要改变禁用按钮的形状或尺寸——只改变颜色和光标。
加载状态
当按钮触发异步操作时,用旋转指示器替换标签并防止重复提交。
.btn--loading {
pointer-events: none;
opacity: 0.7;
}
要点:
- 加载时光标不改变。光标表达的是可供性而非进度:普通内容用箭头,可操作对象用 pointer。进度属于元素本身以及用户已经在看的加载区域,而不是鼠标恰好停留的位置。cursor: wait 在 pointer-events: none 旁边也是惰性的,因此该规则读起来是有意为之,实际不做任何事。
- 加载期间保持按钮宽度稳定——避免标签被旋转指示器替换时发生布局偏移。
- 完成(成功或失败)后回到静止状态。
- 对于长时间运行的操作,配合状态消息——仅靠旋转指示器无法告诉用户正在发生什么。
按下时缩放(可选)
按下时的轻微缩小能增加物理反馈——借鉴自迪士尼的挤压原则。
.btn:active {
transform: scale(0.97);
transition: transform 80ms ease-out;
}
缩放值保持在 0.95–0.98 之间。低于 0.95 会让人感觉按钮要坏了。
完整按钮 CSS 参考
.btn {
cursor: pointer;
background: var(--color-primary);
color: white;
border-radius: var(--radius-button);
padding: var(--component-padding-y-md) var(--component-padding-x-md);
height: var(--component-height-md);
border: none;
transition: background 120ms ease-out, transform 80ms ease-out;
}
.btn:hover { background: var(--color-primary-hover); }
.btn:active { background: var(--color-primary-active); transform: scale(0.97); }
.btn:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 3px; }
.btn:disabled { opacity: 0.4; cursor: not-allowed; pointer-events: none; }
.btn.btn--loading { opacity: 0.7; pointer-events: none; }
评审清单
- 每个交互元素是否都定义了全部六种状态?
- 悬停和按下颜色是否通过亮度调整从基础色派生(而非随意选取)?
- 聚焦状态是否可见并使用 outline(未被移除)?
- 禁用状态是否为低透明度并配合 cursor: not-allowed?
- 加载状态是否防止重复提交?
- 过渡时长是否为 80–150ms——不是瞬间,也不缓慢?
- 所有交互元素在静止状态下是否都显示 cursor: pointer?
- 操作运行期间光标是否保持不变,进度由元素自身显示?