开篇小故事:做按钮设计时,我把默认态颜色定好后,开始处理交互状态。hover 用了一个颜色,pressed 用了一个颜色,disabled 用了另外一个颜色。
我常用的取色方法是打开设计软件的取色器,通过把默认色向深浅两个方向拖拽,找到符合需要的即可。比如把 hover 拉深一点,pressed 再深一点,disabled 把透明度降到 50%。三个状态各花了两分钟,很快就做完了。
然后我开始做卡片组件。卡片也有 hover,pressed,和 disabled。同样的方法,我又打开取色器,重新拉了一遍。拉完之后发现卡片的 hover 态颜色深度和按钮的 hover 态颜色深度不太一样,但说不清楚该不该一样。
再然后是做图标按钮、文字链接、标签页、列表项的三态设计。我继续用同一种方法,每一个组件都有自己的一套交互状态颜色,每一套都是单独调的。等做到第五个组件的时候,我已经不记得第一个组件的 hover 加深了多少?所以只能回去对照,但对照完也没什么用,因为当初并没有建构一个可以对照的标准。每次做完一个新组件,都是在重新创造一遍交互状态的颜色,而这些颜色本来应该从一个统一的规则中推导出来。
hover、pressed、disabled 看起来只是"颜色深浅不同",但它们传递的信息完全不一样。在定颜色之前,最好先搞清楚每个状态都在向用户传递什么。

hover 传递的信息是"这里可以交互"。当用户的光标移到一个元素上方,元素发生了视觉变化,用户由此获得的感知是:这个东西不是静态的,我可以对它做点什么。hover 是一个邀请信号,像门开了一条缝,让人知道可以推开,但人还没推进去。
pressed 传递的信息是"你的操作已经被接收了"。用户按下了鼠标或手指触碰了屏幕,元素立刻给出反馈,告诉用户:收到了,正在响应。pressed 是一个确认信号,反馈力度比 hover 强一个等级。但用户松手之后,这个状态就结束了。
disabled 传递的信息是"这里现在不能操作"。和前两个状态不同,disabled 不是交互过程中的一个环节,而是一个结论:这个元素当前不在可用范围内。disabled 是一个阻止信号,它不想吸引用户的注意力,而是希望用户的视线跳过这里,去找真正能操作的地方。
hover 和 pressed 都在"活跃"的方向上递进,颜色变深、视觉权重变重,传递的是"正在参与交互"的信息。disabled 则在相反的方向上移动,颜色变淡、视觉权重变轻,传递的是"退出交互"的信息。
知道了 hover 和 pressed 要变深、disabled 要变淡,那么接下来的问题就是:深多少?淡多少?这个变化幅度受两个互相矛盾的要求共同约束。
第一个条件是变化量要足够被感知。用户把光标移到按钮上,如果颜色几乎没变,用户会怀疑这个按钮是不是真的可以点击。用户按下了按钮,如果颜色和 hover 一样,用户会怀疑自己到底有没有按到。每一级状态的颜色变化都需要超过人眼的感知阈值,否则状态反馈也就失效了。
第二个条件是变化后的视觉权重不能超过组件在界面层级中的位置。一个次要按钮 hover 之后,颜色变得比主按钮的默认态还深,整个界面的视觉层级就被打乱了。一个列表项 pressed 之后,颜色比页面标题还重,用户的注意力会被错误地拉走。状态变化是在组件自身范围内发生的事,不应该影响到界面整体的视觉秩序。
变化要够大,用户才能感知到反馈;变化又不能太大,组件不能因为状态变化而越级。
这个原则解释了为什么交互状态的颜色变化通常是温和的。hover 的明度变化只有 10-15%,pressed 再多 5-8%,都不是大幅度的跳变。设计师在日常工作中凭感觉调深调浅,调出来的合适范围往往也在这个区间附近。
hover 状态下,用户还没有点击,只是把光标移了过来,元素需要给出一个轻微的视觉反馈,告诉用户:这里有响应,你可以继续。
hover 的颜色变化主要发生在明度这个维度上。在 HSL 色彩模型里,H(色相)和 S(饱和度)保持不变,L(明度)降低 10-15%。色相不变是因为 hover 不改变组件的身份——一个蓝色按钮 hover 之后还是蓝色按钮,不应该变成另一种蓝。饱和度不变是因为 hover 不改变组件的功能语义——主按钮 hover 之后不应该看起来比默认态更鲜艳或更灰暗。只有明度在动,而且只动一小步。
为什么是加深(降低明度)而不是变浅(提高明度)?试想一下,如果 hover 让颜色变浅,用户正在把光标移过来,元素反而在视觉上"退远"了,和靠近的动作方向相反,认知上会很别扭。加深的方向则和物理世界的经验一致:当手指靠近一个物体表面,手指投下的阴影会让物体局部变暗。屏幕上的 hover 效果模拟的就是这种"有东西靠近了"的感觉。加深意味着"有接触的倾向",用户直觉上理解为:我正在接近这个元素,准备和它互动。
具体加深多少取决于组件的默认态颜色。一个中等明度的蓝色按钮(比如 HSL 220, 80%, 55%),hover 态把明度降到 45% 左右,变化量大约 10 个百分点,视觉上是从"蓝色"变成"稍微深一些的蓝色",用户能感知到变化,但不会觉得按钮突然变了一种颜色。

但这个 10-15% 不是所有情况都适用的固定数字。浅色组件和深色组件的处理方式不同。
浅色背景的按钮(比如一个白底蓝字的次要按钮)hover 时不适合直接加深背景色,因为它本身就是浅色的,加深 10% 的效果可能不明显。这类组件的 hover 通常采用另一种方式:叠加一层极浅的背景色。比如在白色背景上叠一层 5-8% 不透明度的主色,形成一个若有若无的色调变化。用户的感知是"这个区域有点不一样了",而不是"颜色变深了"。
深色实心按钮(比如深蓝底白字的主按钮)hover 时直接降低明度就够了。从 HSL 的 L 值 45% 降到 35-38%,变化清晰,和默认态的层级也不冲突。
还有一类元素需要说明:卡片、列表项这类容器元素。它们的 hover 通常不是改变自身颜色,而是在底部叠加一层浅灰色或极低不透明度的黑色蒙层。原因是容器内部有文字、图标等子元素,如果容器本身颜色变化太大,子元素的对比度也会跟着变,阅读体验会受影响。叠加一层淡淡的蒙层作为 hover 反馈,容器的视觉变化足够被感知,内部内容的可读性也不会受影响。
还有一个细节会影响 hover 的感受:过渡动画的速度。hover 颜色变化通常配合 150-200ms 的 ease-in-out 过渡。太快(50ms 以下)会让变化看起来像界面闪烁,太慢(400ms 以上)会让用户觉得界面迟钝。速度不改变颜色本身,但会改变用户对 hover 反馈的整体感知。
pressed 是当用户按下鼠标或触碰屏幕的那一刻的界面反馈状态,和 hover 不同,pressed 是发起了一个明确的交互动作,用户不再只是"靠近",而是实实在在的"按下去了"。所以颜色变化需要反映这种力度上的差异。
pressed 的颜色变化方向和 hover 一致,仍然是在明度维度上继续加深。从默认态的明度值出发,hover 降低了 10-15%,pressed 在 hover 的基础上再降低 5-8%,总计相对默认态降低约 15-20%。色相和饱和度依然保持不变。

为什么 pressed 只比 hover 多降低 5-8%,而不是跳到一个明显更深的位置?这和 pressed 状态的时间特性有关。用户按下按钮到松手,这个过程通常只有 100-200 毫秒。在这么短的时间里,颜色变化太大会产生闪烁感——用户看到一个颜色瞬间变深又瞬间恢复的过程,视觉上像在"闪",这会让用户感到界面不稳定,甚至以为界面出了问题。pressed 的颜色变化如果设置的温和一些,那么用户的感知到的是"按下去有了反馈",而不是"颜色突然跳了一下"。
在物理世界中,按压一个按键的视觉效果是:按键表面下沉,光照角度变化,导致颜色整体变暗一点。这和 pressed 加深明度的方向是一致的。有些设计系统还会在 pressed 态加上 1-2px 的内阴影(inset shadow),模拟按键表面下沉的深度感。这种做法不影响颜色选择本身,但在视觉效果上可以强化"按下去了"的感知,让颜色变化不需要太大也能传递足够用力的反馈。
pressed 和 hover 之间的颜色差异需要把握一个尺度。差异太小,用户感觉"按了没反应",以为点击没有被系统接收,于是可能会反复点击。差异太大,每次按下都会产生色差跳变,在频繁操作的场景中(比如反复点击列表项)会让界面显得不稳定。
一个简单的检验方法,是把 hover 态和 pressed 态的颜色并排放在一起比较。如果两者放在一起几乎分不清楚,说明差异太小;如果两者放在一起像是完全不同的两种颜色,则说明差异太大。
对于浅色组件,pressed 的处理方法和 hover 类似但幅度更大。hover 时叠加的是 5-8% 不透明度的主色蒙层,pressed 时蒙层不透明度可以提升到 12-18%。变化方向一致,幅度递进,用户能清楚感知到"hover → pressed"是一个连续的过程。
在为移动端进行状态设计时,有一个特殊情况需要格外注意。由于移动设备上没有光标悬停的概念,触摸操作直接从默认态跳到 pressed 态,中间没有 hover 过渡。这意味着 pressed 的颜色变化相对默认态的反差需要足够明确。
在桌面端,用户从 hover 到 pressed 是一个渐进过程,心理上已经有了"这个元素正在响应我"的预期;在移动端,pressed 是用户收到的第一个视觉反馈,如果变化太微妙,用户可能完全注意不到。所以在做移动端状态适配时,pressed 的明度变化量可以适当放大到 18-22%,以确保触摸反馈足够清晰。

disabled 和前两种状态的取色逻辑完全不同。hover 和 pressed 都是用户正在和组件互动时发生的反馈,颜色在"更活跃"的方向上逐渐递进。disabled 所做的事情正好相反,它是让组件从交互世界中退出,告诉用户"这里不可点击"。
"退出"在视觉上意味着两件事同时发生:颜色失去功能含义,元素失去视觉权重。对应到操作上,就是降低饱和度和提高明度(或增加透明度)。
当一个蓝色按钮的饱和度从 80% 降到 20%,蓝色几乎消失,变成一种灰蓝色。用户看到这种颜色时,不会再把它当作"主操作按钮"来处理,因为颜色已经不再传递"这是主操作"的信号了。饱和度的降低直接削弱了颜色的功能含义,用户的认知从"这是一个可以执行的蓝色按钮"变成"这是一个灰灰的东西"。
但只降饱和度还不够。如果 disabled 的元素饱和度降了但明度不变,它在视觉上仍然占据一定重量,用户的目光会在这里停留,然后发现不能操作,从而白白浪费了注意力。明度提高之后,disabled 元素在视觉上退到和背景接近的层级,用户的视线自然会跳过它,去找那些能够操作的界面元素。disabled 的取色目标不只是"让用户知道这里不能操作",还应该包含"让用户不要在这里停留"。
第一种做法是直接使用透明度。把组件的整体透明度降到 40-50%,背景色、文字、图标一起变淡。这种做法的优势是简单——一个属性值搞定,不需要为组件的每个部分单独计算颜色。但它有一个问题:透明度是相对背景计算的,同一个 50% 透明度的元素放在白色背景上和放在浅灰色背景上,最终呈现的颜色不一样。如果组件会出现在不同背景上,透明度方案会导致 disabled 态的视觉效果不一致。
第二种是替换为固定色值。把按钮的蓝色背景替换为一个饱和度极低、明度极高的灰蓝色(比如 HSL 220, 15%, 88%),文字颜色替换为一个浅灰色(比如 HSL 0, 0%, 70%)。这种做法的优势是可控——disabled 态在任何背景上看起来都是一样的,颜色不受环境影响。代价是需要为每种颜色分别定义 disabled 态的色值。

这两种路径不完全是互斥的。在很多设计系统中,实心按钮(filled button)使用固定色值方案,因为按钮作为独立组件在不同背景上都要保持一致的视觉效果。而容器元素(卡片、面板)的 disabled 态常使用透明度方案,因为容器通常停留在固定的背景上,透明度的环境依赖问题不突出,简单设置一个整体透明度就够用了。

一个需要特别注意的地方是 disabled 态的文字颜色。很多设计师在做 disabled 时只改了按钮背景色,忘了同步调整文字颜色。结果是:背景已经变成浅灰了,文字还是白色,对比度反而变高了,按钮看起来文字很清楚但就是不能点击,这种状态在视觉上是矛盾的。用户看到清晰的文字,本能反应是"这是可以读的、可以操作的",然后发现点不了,会感到困惑。正确的做法是背景和文字一起变淡:背景降饱和提明度,文字颜色也换成和背景接近的浅灰色,两者之间保留刚好能辨认文字内容的对比度(大约 2:1 到 2.5:1),让用户知道按钮上写的是什么,但不会误以为它是可操作的。

disabled 态还有一种容易被忽视的子场景:深色模式下的 disabled。在浅色模式下,disabled 通过提高明度让元素"消融"进浅色背景中。在深色模式下,背景本身是深色的,如果还是提高明度,disabled 元素反而会比周围的深色背景更亮,变得更显眼,效果完全反了。深色模式下的 disabled 应该降低明度,让元素进一步"沉"进深色背景里。同时饱和度依然要降低,方向不变。浅色模式下 disabled 往"亮"的方向退,深色模式下 disabled 往"暗"的方向退,两者的目标一致——让 disabled 元素在视觉上尽量接近背景,不再吸引注意力。
不管是主按钮、次要按钮、文字链接、图标按钮、标签页还是卡片,交互状态的颜色选择都可以从默认色出发,按同一套步骤推导。
第一步,确定组件的默认态颜色。把它转换成 HSL 值,记下 H(色相)、S(饱和度)、L(明度)三个数值。后续所有变化都基于这组数值推导。
第二步,生成 hover 态。H 不变,S 不变,L 减少 10-15%。如果组件是浅色背景的(比如白底蓝字的次要按钮),不要直接改底色的 L 值,改为在默认态底色上叠加一层 5-8% 不透明度的主色。
第三步,生成 pressed 态。H 不变,S 不变,L 在 hover 的基础上再减少 5-8%,相对默认态总共减少 15-20%。如果是浅色组件,蒙层的不透明度从 hover 的 5-8% 提升到 12-18%。移动端场景下,因为没有 hover 过渡,pressed 的明度变化量可以在桌面端的基础上再放大 3-5%。
第四步,生成 disabled 态。这里有两条路径,根据组件类型选择。
如果是实心按钮(filled button),使用固定色值方案:H 不变,S 降低到原值的 30-40%(比如原来 S 是 80%,降到 24-32%),L 提高到 85-90%。文字颜色同步替换为浅灰色,和背景之间保留 2:1 到 2.5:1 的对比度。
如果是容器元素(卡片、面板、列表项),使用透明度方案:整体透明度设为 40-50%。确保组件通常出现在固定背景上,避免透明度带来的背景依赖问题。
第五步,把四个状态排在一起校验。
1. 四个状态之间的差异是否可感知?把默认、hover、pressed、disabled 四种颜色并排放,每相邻两个之间是否能看出区别。特别注意 hover 和 pressed 之间——这两者的差异最小,也最容易做得太接近以至于分不清。
2. 状态变化后是否越级?把组件放回界面上下文里,hover 态的按钮是否比它上方的主标题更抢眼?pressed 态的次要按钮是否比旁边默认态的主按钮更深?如果有越级,把变化幅度缩小,或者回过头检查组件默认态的颜色在界面层级中是否本身就偏重了。
第六步,应用到同类组件。用同一套规则处理同一个组件库中的其他组件。蓝色主按钮用这套规则推导出了四个状态色,红色危险按钮也用同样的百分比推导——H 从蓝变成红,S 和 L 的变化方式一模一样。这样做出来的组件库,所有颜色变化的节奏和幅度是一致的,用户在使用产品时不会因为不同组件的交互反馈强弱不同而产生认知负担。
还有一个场景容易漏掉:hover 和 selected(选中态)同时出现时该怎么办。比如一个侧边栏导航项,当前页面对应的选项已经处于 selected 态(通常是浅色主色背景),用户把光标移到这个已选中的选项上,hover 效果是否还要叠加?大多数设计系统的处理方式是:selected 态的 hover 幅度缩小到正常 hover 的一半左右,甚至不做 hover 变化。原因是 selected 态已经传递了"这里是当前位置"的信息。用户不需要被告知"这个已经选中的选项可以被点击",因为它已经在起作用了。
最后,我们再回到开头的故事里,当时我每做一个组件就重新想一遍交互状态的颜色,就是因为没有一套从默认色推导状态色的规则。有了这套规则之后,第一个按钮只需理解原理即可,从第二个开始就可以按这样的逻辑进行推导:默认色转成 HSL,hover 减明度 10-15%,pressed 减 15-20%,disabled 降饱和加明度。所有组件的交互反馈节奏保持一致,色值表里每个状态颜色都能说清楚怎么来的,新加入的设计师也只需看一遍就能上手。
有0人收藏了本文