button-states

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Button and Interactive Element States

按钮与交互元素状态

Every interactive component must have a complete, visually distinct state for each interaction mode. Missing or ambiguous states make the UI feel unfinished and reduce user confidence.
每个交互组件都必须为每种交互模式定义一个完整、视觉区分度清晰的状态。缺失或模糊的状态会让UI显得不够完善,降低用户的使用信心。

The Six States

六种状态

StateTriggerVisual signal
RestDefaultBase colour, cursor: pointer
HoverMouse overSlightly darker, subtle background shift
Active / PressedMouse down / tapNoticeably darker, slight scale-down
FocusKeyboard navigationVisible focus ring, no change to fill
DisabledNot availableLow contrast, cursor: not-allowed, no interaction
LoadingAsync action in progressSpinner or pulse, non-interactive
状态触发条件视觉信号
默认态(Rest)默认状态基础颜色,光标:pointer
悬停态(Hover)鼠标悬停颜色略深,背景轻微变化
激活/按压态(Active / Pressed)鼠标按下/点击颜色明显加深,轻微缩小
聚焦态(Focus)键盘导航可见的聚焦环,填充色无变化
禁用态(Disabled)不可用低对比度,光标:not-allowed,无法交互
加载态(Loading)异步操作进行中加载动画或脉冲效果,无法交互

Deriving State Colours Algorithmically

通过算法衍生状态颜色

State colours are not chosen independently — they are derived from the base colour by adjusting lightness in HSL. This guarantees coherence across the entire palette.
base:     hsl(H, S%, L%)
hover:    hsl(H, S%, L% - 8%)    ← darken 8%
active:   hsl(H, S%, L% - 14%)   ← darken 14%
状态颜色并非独立选择——它们通过调整HSL中的亮度值从基础色衍生而来,这能确保整个调色板的一致性。
base:     hsl(H, S%, L%)
hover:    hsl(H, S%, L% - 8%)    ← 加深8%
active:   hsl(H, S%, L% - 14%)   ← 加深14%

Example: primary button
#635BFF
(hsl 243, 100%, 68%)

示例:主按钮
#635BFF
(hsl 243, 100%, 68%)

css
.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 */
}
For light buttons on dark backgrounds, invert the logic — lighten on hover instead of darkening.
css
.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 */
}
对于深色背景上的浅色按钮,需反转逻辑——悬停时提亮而非加深颜色。

Secondary / outlined buttons

次要/轮廓按钮

css
.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);
}
css
.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);
}

Focus State

聚焦态

Focus is a keyboard navigation requirement (WCAG 2.2). It must be visible and must not rely on the hover style alone — keyboard users do not trigger hover.
css
.btn:focus-visible {
  outline: 2px solid var(--color-primary);
  outline-offset: 3px;
}
  • Use
    outline
    , not
    box-shadow
    , for focus rings —
    outline
    respects
    border-radius
    in modern browsers and does not affect layout
  • outline-offset: 2–4px
    gives the ring breathing room from the component edge
  • Never use
    outline: none
    without a replacement focus style
聚焦是键盘导航的要求(WCAG 2.2)。聚焦状态必须可见,且不能仅依赖悬停样式——键盘用户不会触发悬停。
css
.btn:focus-visible {
  outline: 2px solid var(--color-primary);
  outline-offset: 3px;
}
  • 使用
    outline
    而非
    box-shadow
    实现聚焦环——现代浏览器中
    outline
    会遵循
    border-radius
    ,且不会影响布局
  • outline-offset: 2–4px
    让聚焦环与组件边缘保持一定间距
  • 禁止在未提供替代聚焦样式的情况下使用
    outline: none

Disabled State

禁用态

css
.btn:disabled,
.btn[aria-disabled="true"] {
  opacity: 0.4;
  cursor: not-allowed;
  pointer-events: none;
}
  • Disabled elements are exempt from WCAG contrast requirements — low opacity is correct and intentional
  • Use
    pointer-events: none
    to prevent click events even if JS is bypassed
  • Do not change the shape or size of a disabled button — only colour and cursor change
css
.btn:disabled,
.btn[aria-disabled="true"] {
  opacity: 0.4;
  cursor: not-allowed;
  pointer-events: none;
}
  • 禁用元素不受WCAG对比度要求限制——低透明度是正确且有意的设计
  • 使用
    pointer-events: none
    防止即使JS失效时仍触发点击事件
  • 禁用按钮的形状或尺寸不应改变——仅调整颜色和光标

Loading State

加载态

When a button triggers an async action, replace the label with a spinner and prevent re-submission.
css
.btn--loading {
  pointer-events: none;
  cursor: wait;
  opacity: 0.7;
}
  • Keep the button width stable during loading — avoid layout shift when label is replaced by spinner
  • Return to rest state on completion (success or error)
  • For long-running operations, pair with a status message — a spinner alone does not tell the user what is happening
当按钮触发异步操作时,将标签替换为加载动画,并防止重复提交。
css
.btn--loading {
  pointer-events: none;
  cursor: wait;
  opacity: 0.7;
}
  • 加载过程中保持按钮宽度稳定——避免标签被替换为加载动画时出现布局偏移
  • 操作完成(成功或失败)后恢复为默认态
  • 对于长时间运行的操作,需搭配状态提示信息——仅加载动画无法告知用户当前正在进行的操作

Scale on Active (Optional)

激活时缩放(可选)

A subtle scale-down on press adds physical feedback — borrowed from Disney's squash principle.
css
.btn:active {
  transform: scale(0.97);
  transition: transform 80ms ease-out;
}
Keep the scale value between
0.95–0.98
. Below
0.95
feels like the button is breaking.
按压时轻微缩小能提供物理反馈——灵感来自迪士尼的挤压原则。
css
.btn:active {
  transform: scale(0.97);
  transition: transform 80ms ease-out;
}
缩放值应保持在
0.95–0.98
之间。低于
0.95
会让按钮看起来像是“断裂”了。

Complete Button CSS Reference

完整按钮CSS参考

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; cursor: wait; pointer-events: none; }
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; cursor: wait; pointer-events: none; }

Review Checklist

检查清单

  • Does every interactive element have all six states defined?
  • Are hover and active colours derived from the base by lightness adjustment (not chosen arbitrarily)?
  • Is focus state visible and using
    outline
    (not removed)?
  • Is disabled state low-opacity with
    cursor: not-allowed
    ?
  • Does loading state prevent re-submission?
  • Are transition durations 80–150ms — not instant, not slow?
  • Does
    cursor: pointer
    appear on all interactive elements at rest?
  • 每个交互元素是否都定义了全部六种状态?
  • 悬停和激活颜色是否通过调整亮度从基础色衍生(而非随意选择)?
  • 聚焦状态是否可见且使用
    outline
    (未被移除)?
  • 禁用状态是否为低透明度并设置
    cursor: not-allowed
  • 加载状态是否防止重复提交?
  • 过渡时长是否在80–150ms之间——既不突兀也不缓慢?
  • 所有交互元素在默认态下是否显示
    cursor: pointer