前端开发者学堂 - fedev.cn

A11Y 101:颜色对比度和Web可访问性

发布于 大漠

时至今日,Web页面或Web应用上色彩都是很丰富的。一般情况之下,开发者给Web上色大多是根据视觉设计师提供的设计稿来定义。颜色除了能决定Web在视觉上的美观之外,还能直接影响Web可访问性。因为访问或使用你产品的用户可能会在较亮的环境下(比如阳光下),或设备陈旧上(比如低分辨率显式器),颜色可以直接决定用户能否较好的访问。除此之外,可能有一些视觉障碍的用户(比如视弱,色盲)使用你的产品,这群用户群体同样会受颜色影响。

事实上,不管是视觉设计师或者Web开发者,在颜色使用上都存有一定的误区。简单地说,Web上前景色(color)和背景色(background-color)两颜色对比度没有达到一定的程度,造成可访问性较差。而且这种现象在众多Web应用上都有,WebAIM的报告就能很好的说明这一现象:

颜色对比是Web可访问性(A11Y)的一个重要方面。良好的对比度使用有视觉障碍的人更容易使用产品,并有助于在不完美的条件下较好的使用产品

考虑到这一点,不管是Web开发者还是视觉设计师都有必要掌握这方面的知识,只有这样才能构建出体验更好的Web。

用户体验和Web可访问性

我们就从大家常说的用户体验(UX)和Web可访问性(A11Y)开始吧。我知道这两者的定义都很复杂,而且专家们对这两者都有过相应的定义。这里我想说说我自己对这两者的理解。

用户体验是一个人对产品或服务的体验。根据你想要达到的目标,这种体验可以是好的也可以是坏的。通常情况下,设计师总是希望自己能提供一种令人难忘的体验,以一种好的方式让用户更易于理解、使用和喜爱你的产品或界面。要让产品界面可用,就需要了解特定的人群,这样才能帮助他们高效、有效、满意地完成精确的任务。

Web可访问性是让尽可能多的人使用Web网站或Web应用。我们通常认为它只适用于残疾人士(早期定义就是无障碍设计),但是在某些特定的情况或环境中对其他群体的人也有益。

当用户体验趋向于满足一个精确的人群时,Web可访问性趋向于在一个公共的良体验中包含尽可能多的人,但是两者并不相互排斥。

用户体验和Web可访问性都是较大的两个领域,各自领域都有着自己专业方面的知识和技能。但它们的目的都是为了给用户提供最好的产品。虽然两个领域有差异,但它们也有一些共同点,是相辅相成的。比如我们本文要说的颜色对比度就是一个很好的例子。

但不管是设计师还是Web开发者在使用颜色时都被一些“神话”给误导,却不能很好地理解在什么情况下使用颜色对比标准。不仅如此,他们还认为,当使用颜色对比来传达信息时,界面是不可访问的。

接下来的内容,可以帮助设计师和Web开发者打破这种神话,能更好的帮助大家理解颜色对比度和如何使用。

色彩理论和对比度

颜色的定义是个复杂的系统,平时我们描述一种颜色时总是喜欢和另一种颜色结合起来描述,比如同样是描述红色,你可能会说这种红色是砖红色还是消防车红色。这也是表达颜色的自然方式,但要准确定义某种颜色是什么,这几乎不太可能。从本质上讲,颜色是一种相对的个人体验

每种颜色都有一个独特的波长。我们的眼睛接收和处理这些波长并将它们转换成颜色。

这是个复杂的体系,我也整不明白,就不误人子弟了。咱们就了解一点基础的理论知识,有助于后面更好的理解。

虽然颜色是相对的,但在组织颜色方面已经有了大量的研究和实战。在《图解CSS: CSS 颜色》和《JavaScript中的颜色转换》中也提到一些色彩空间和色彩模式。特别是@jlfwong的《Color: From Hexcodes to Eyeballs》一文,对这方面做了详细的阐述。这里我们主要来了解一下颜色感知方面。

我们平时所说的颜色,也被称为色调(也有人称为色相)。每一种颜色都对应色谱上的一个点:

如果你接触过HSL颜色模式的话,它指的就是色盘轮上的每个角度:

要使用颜色,需要了解以下不同的属性。

值(Value):从黑色到白色的范围。

如果色调被认为是一个维度围绕着的一个轮子,那么值就是一个通过轮子中间的线性轴:

对比度(Contrast):值之间的分离程度:

亮度(Brightness):指颜色的亮度,不同的颜色具有不同的明度,比如图像缺乏亮度会使图像色调变淡:

明度(Lightness):在色彩理论中也称为值或色调,是对颜色或色彩空间亮度知变化的一种表示:

饱和度(Saturation):根据一个区域的亮度比例来判断该区域的色彩,它实际上是来自该区域的光线的白色度的感知自由。缺少饱和度的颜色变得更灰。

色度(Chroma):指的是“一个区域的色度,根据亮度与白色或高透射率区域的亮度之比来判断”。因此,色度大多只取决于光谱性质,并以此来描述物体的颜色。

上面几个属性对颜色有着直接影响。

另外颜色和颜色混合在一起会生成新的一种颜色:

目前颜色的混合主要有两种方式:加色法减色法

减色法是青色(cyan)、品红(magenta)和黄色(yellow)混合在一起,就得到一种近似黑色的颜色。这是因为这种方法使用反射色。这些颜色是使用一种特理物质,就像油漆中的颜料,可以将波长反射到眼睛里。去掉这些颜色或它们的缺失,就只剩下白色(或画布上的任何颜色)。

加色法是红色(red)、绿色(green)和蓝色(blue)三种基色的叠加。当这些颜色以不同的组合方式组合在一起时,就会产生其他颜色。这三种颜色组合在一起就会产生白光。简而言之,这就是如何在你的智能设备或电脑显示器上实现颜色。

我们这里重点关注的是颜色对比度,即Contrast

什么是颜色对比度

对比度最简单的描述就是两种颜色在亮度(Brightness)上的差别。理解对比度的一个好方法是比较色调相同的颜色。他们越接近,他们之间的对比度越低:

除了在灰色调做对比之外,还可以在不同色调的颜色之间做对比:

简单地说,对比度解释了在给定范围内最亮颜色亮度和最暗颜色亮度之间差异。它是每种颜色的相对亮度(Relative Luminance),是标准化的值,从最黑的0值到最亮的1值。

要想更好的理解颜色对比度是需要使用一些数学知识。庆幸的是,W3C规范已经为社区提供了帮助分析同时使用的两种颜色的公式。规则中提供的将RGB值转换为相对亮度的公式如下所示:

简单地解释一下。

对于sRGB颜色空间,颜色的相对亮度(Luma)的计算公式:

Luma = 0.2126 * R + 0.7152 * G + 0.0722 * B

其中RGB通道值的校正细节:

最终得到sRGB颜色空间中的RGB的值:

有了一种确定颜色相对亮度的方法,就可以用所谓的颜色对比度来比较它们:

我们来看一个简单的示例:

比如上图中的按钮,背景色颜色值是#ff4400,文本颜色值是#ffffff。如果要计算出这两种颜色的相对亮度,首先要得到颜色的RGB表示法。取出对应颜色的RGB颜色通道的值:

#ff4400  ❯❯❯ rgb(255 68 0)    ❯❯❯ R = 255, G = 68, B = 0
#ffffff  ❯❯❯ rgb(255 255 255) ❯❯❯ R = 255, G = 255, B = 255

有关于颜色转换的具体方法可以阅读《JavaScript中的颜色转换》一文。也可以直接使用拾色器直接取出颜色RGB的值。

接下来将RGB各通道的值除以255,就可以得到对应的线性值:

#ff4400  ❯❯❯ rgb(255 68 0)
|  
| ❯ R = 255 ❯❯❯ 255 / 255 = 1 
| ❯ G = 68  ❯❯❯ 68  / 255 = 0.26666667
| ❯ B = 0   ❯❯❯ 0   / 255 = 0

#ffffff  ❯❯❯ rgb(255 255 255)
|  
| ❯ R = 255 ❯❯❯ 255 / 255 = 1 
| ❯ G = 255 ❯❯❯ 255 / 255 = 1
| ❯ B = 255 ❯❯❯ 255 / 255 = 1

然后,需要对RGB颜色各通道的值进行校正,一般采用伽玛校正(它定义了像素的数值和它的实际亮度之间的关系)。

伽玛校正的作用简单地说,它将计算机“看到”的东西转化为人类对亮度的感知。计算机直接记录光,其中两倍的光子等于两倍的亮度。人的眼睛在昏暗的条件下会感知到更多的光线,而在明亮的条件下则会感知到更少的光线。我们周围的数字设备一直在进行伽玛编码和解码计算。它被用来在屏幕上向我们展示与我们对事物在我们眼中的感觉相匹配的东西。

不了解伽玛校正也不用担心,我们可以直接按照下面的公式来对RGB值进行校正:

先来看#ff4400颜色:

R = 1 ❯❯❯ R > 0.03928 ❯❯❯ ((R + 0.055) / 1.055)^2.4 = ((1 + 0.055) / 1.055) ** 2.4 = 1
G = 0.26666667 / 12.92 ❯❯❯ G > 0.03928 ❯❯❯ ((R + 0.055) / 1.055)^2.4 = ((0.26666667 + 0.055) / 1.055)^2.4 = 0.05780543
B = 0 ❯❯❯ B <= 0.03928 ❯❯❯ B / 12.92 = 0 / 12.92 = 0

#ffffff颜色的RGB正好都相等,值为1,大于0.03928,计算出来的幂值为1

^2.4是一个幂值,可以用JavaScript中**幂 (**)**运算符

最后,我们用数字来表示颜色在人眼中的亮度,我们可以下面的公式来计算:

Luma = 0.2126 * R + 0.7152 * G + 0.0722 * B

根据上面的公式,我们就可以计算出#ffffff#ff4400对应的相对亮度的值,为了区分这两者,我们分别用Luma1Luma2来表示:

#ffffff ❯❯❯ Luma1 = 0.2126 * R + 0.7152 * G + 0.0722 * B = 0.2126 * 1 + 0.7152 * 1 + 0.0722 * 1 = 1
#ff4400 ❯❯❯ Luma2 = 0.2126 * R + 0.7152 * G + 0.0722 * B = 0.2126 * 1 + 0.7152 * 0.05780543 + 0.0722 * 0 = 0.25394244

这样就得到了计算颜色对比度所需要的Luma1Luma2的值。为了确定哪个值是Luma1,哪个值是Luma2,我们需要确保较大的数字(亮色)始终是Luma1,将较小的数字(暗色)作为Luma2。然后根据对比度计算公式:

R = (Luma1 + 0.05) / (Luma2 + 0.05) = (1 + 0.05) / (0.25394244 + 0.05) = 1.05 / 0.30394244 = 3.4546

#ff4400#ffffff两颜色的对比度的值约为3.4546

现在将这个结果与WCAG指南中颜色对比度准则进行比较。就上图示例中来说,按钮的文本字号19pt,而且加粗,属于大号文本规范:

当文本字号(font-size)大于18pt(大约24px)或大于14pt(大约19px)粗体时,文本被认为是大号的。大号字体需要达到3:1的对比度才能通过AA级,4.5:1的对比度才能通过AAA级。

#ff4400#ffffff的对比分大约是3.4546:1,达到了AA级标准。如果你不放心整个计算的结果,那么可以使用在线工具来验证:

你也可以用JavaScript脚本构建一个关于颜色对比度检测的工具:

不过,@Andrew Somers通过研究和实践发现,该算法提供了错误的结果

目前,Silver Task Force正在研究一种新的检测对比度的模型,由于当前的对比度比率不是一种理想的算法,Silver Task Force正在研究一种新的可达性准则

颜色对比度准则

Web可访问性(A11Y)服务的群体并不是仅服务于有残疾的人士,也服务于很多正常人士,因为在一些特定的环境也会影响正常人士正常访问或使用你的Web应用。而颜色的对比度更是如此,因为一个视力低或色盲的人在没有足够对比度的情况下无法区分文本,甚至对于视力正常的人在特定环境下(比如阳光下,夜晚中)也存有这方面问题。

另外,不要忘记感知颜色是一个包括大脑在内的过程。所以人们可能会有认知问题,这可能会影响他们对颜色的感知。

在Web的环境中,WCAG(Web可访问性指南)规范中有许多关于颜色使用的指南。分别是:

这些指南可以很好的帮助我们如何更好的在Web中处理颜色对比度。

WCAG指南中将颜色对比度分为三个级别:AAAAAA。例如,如果一个网站满足了AA级别的所有要求,那么它就满足了AA级别。在对比度方面,主要分为文本对比度AAAAA)和非文本对比度(仅AA)的标准。

文本的对比

在涉及到文本(或文本的图像)时,需要在文本和背景颜色之间有足够的对比度。可接受的对比度有两个级别:**AA**和 AAA。根据文本的大小,这个级别的定义有所不同。

小号文本

当文本字号(font-size)小于18pt(大约24px)或小于14pt(大约19px)粗体时,文本被认为是小号的。小号字体需要达到4.5:1的对比度才能通过AA级,7:1的对比度才能通过AAA级。

大号文本

当文本字号(font-size)大于18pt(大约24px)或大于14pt(大约19px)粗体时,文本被认为是大号的。大号字体需要达到3:1的对比度才能通过AA级,4.5:1的对比度才能通过AAA级。

非文本的对比

用户界面组件与相邻背景的对比度至少需要达到3:1的对比度,才能达到AA级:

此外,以下几个场景,颜色对比度分数至少要达到 3:1

  • 图标和表单元素
  • 图形对象(比如图表)
  • 元素的焦点、悬浮和激活状态

有关于WCAG对颜色对比度标准的规定,可以阅读上面提到的相关规范。规范中针对不同的场景做出详细的介绍

这里推荐大家阅读:

WCAG标准的困惑

WCAG对于颜色对比度的规则虽然描述的非常清楚,但你有可能误解了规则中的一些含义。@anthony的《The Myths of Color Contrast Accessibility》一文就列出了八条。

非常感谢@anthony带来的分享

@anthony的文章指出了一些WCAG指南给使用者带来的一些误解,而且围绕着设计师社区展开。我也仔细阅读了这篇文章,发现了其中的一些问题。这些问题不在于@anthony说了什么,而在于他说话的方式 —— 这让你觉得不应该相信最著名的WCAG,因为它不是用户需要的

这篇文章通过几个示例来指导读者,每个示例都提供了两个方案,其中一个是可访问的,另一个是不可访问的,并认为不可访问的解决方案是残障人士的首选。这就导致了一个错误的,危险的结论:不可访问的方案可能是更好的(最好的?)解决方案吗

虽然@anthony的文章提出了不少属于自己的见解,但这些观点并不是每一个人都认同。正如@Geoffrey Crofte在他的《There is no “Myths of Color Contrast Accessibility”》文章中就提出了自己的看法。

我花了一天的时间阅读了这两篇文章,也努力的去理解他们两的观点和相关的剖析,其中有些观点还是值得我们思考的。在这里就不做详细的阐述,感兴趣的可以将这两篇文章做对比性的阅读。

当我有足够的能力理解和掌握了里面的知识,我将会以我自己的角度和思考方式来阐述自己的看法

关于这方面,不在这里花过多的篇幅来阐述,我只想说:WCAG是一套关于Web可访问性的指南,但它更是用来帮助设计人员和开发人员构建更好的,更具可访问的Web页面。更是指导原则

颜色对比度检测工具

如果你阅读到这里的话,说明你对WCAG中颜色对比度准则有所了解,也知道了如何通过一些数学公式来计算颜色的对比度。有了这些基础,你可以借助JavaScript的能力(或者其他编程语言能力)构建颜色对比度检测工具。即使你在这方面不太熟悉或者不想花过多时间来撸码,也不用着急,在社区中有很多优秀的在线工具或者Web应用程序,可以为我们所用,帮助我们快速检测颜色对比度和自动生成具有可访问性的颜色。

这里给大家推荐一些检测工具:

其中Accessible Color GeneratorLeonardoColorBoxColorCube可以帮助我们快速生成具有可访问性的颜色系统。

@Nate Baldwin的《Leonardo: an open source contrast-based color generator》和《Creating contrast-based themes with Leonardo》两篇文章详细介绍了Leonardo如何生成可访问颜色系统。另外,推荐@Kevyn Arnott的《Re-approaching Color》的一篇文章,文章中聊了Lyft设计团队是如何得新审视他们在应用程序中使用颜色的方式。

如果你是设计师的话,也可以给设计软件(比如,Sketch、Figma等设计软件)添加一些插件,这些插件可以很好的帮助你管理颜色和检测颜色对比度,比如Stark

更多的插件:

对于开发者而言,在检测Web可访问性时,颜色对比度会直接影响到A11Y的评分。在现代浏览器中都具备这方面的能力:

在Chrome浏览器中还可以使用拾色器随时检测颜色的对比度是否达到WCAG的标准:

除此之外,还有很多优秀的浏览器插件也可以用于颜色对比度的检测:

不同的检测插件,有不同的检测方式,如下图所示:

你可以根据自己的喜好做出选择。在Firefox浏览器中同样有相应的检测插件,这里不做阐述。

设计可访问的颜色系统

对于设计师来说,基本上都会有符合自己产品品牌色颜色规范体系。

设计一个符合可访问的颜色系统并不是件易事,需要的专业知识较多,特别是对颜色空间、颜色模式相关知识

这里简单地说一下,如何来设计可访问的颜色系统。

回到颜色中,电脑显示器屏幕一般使用RGB颜色色彩空间来指定,而且我们也习惯于这种方式。即RGB三原色通道混合得到一个颜色:

不幸运的是,虽然用这种方式来描述颜色对电脑显示器屏幕来说很自然,但对人类来说却不自然。给定一个RGB颜色值,需要改变什么才能使它更亮呢?颜色更饱满呢?这些等等,都不是那么直观的。

庆幸的是,在色彩空间中有其他的色彩模式能让人类更直观的知道一个颜色。换句话说,一个颜色主要由三个属性组成:

  • 色相(Hue,也被称为色调,可以直观的告诉人类是什么颜色
  • 色度(Chroma,也被称为色距,可以直观的告诉人类颜色有多鲜艳
  • 亮度(Lightness,也被称为明度,可以直观的告诉人类颜色有多亮

支持以这种方式指定颜色的颜色空间有HSL。不过,HSL计算亮度的方法有一定的缺陷。大多数色彩空间没有考虑到的是,不同的色调在本质上被人眼感知为不同程度的明度。换句话说,在数学上明度相同的水平上,黄色比蓝色更亮。

下图是一组在显示颜色空间中具有相同亮度(Lightness)和饱和度(Saturation)的颜色。虽然色彩空间声称饱和度和亮度都一样的,但我们的眼睛不同意。请注意,其中一些颜色看起来比其他颜色更淡或更饱和。例如,蓝色显得特别暗,而黄色和绿色显得特亮。

也有一些色彩空间试图模拟人类对颜色的感知。感知上一致的色彩空间基于人类视觉更相关的因素对颜色进行建模,并执行复杂的颜色转换以确保这些维度反映人类视觉的工作方式。

当我们在一个感知上均匀的颜色空间中选取亮度和饱和度相同的颜色样本时,我们可以观察到明显的差异。这些颜色混合在一起,每一种颜色看起来都和其他颜色一样轻,一样饱和。这就是工作中的感知一致性。

虽然HSL让人类能更直观的描述颜色,但社区也有另一种说法,那就是HSB(也称为HSV)色彩空间要比HSL更易于理解,使用时能够获得更明确的预期。

HSB(色相Hue、饱和度Saturation,明度Brightness),HSV(色相Hue、饱和度Saturation,值Value。有关于HSBHSV更详细的介绍可以阅读《CSS颜色》一文。

目前在社区中,一般都在使用HSB(或HSV)模型来做颜色转换算法,生成完整的渐变色板。比如Ant Design 3.x用的就是这种模式。如果你熟悉JavaScript和HSB颜色模式相关算法的,也可以用脚本来构建一套适合自己产品品牌颜色的体系。

不过话说回来,即使使用HSV颜色模式来生成颜色面板,同样会存在颜色可访问性达不到WCAG相关的标准。@董文博老师的《Ant Design 色板生成算法演进之路》一文就特别提到过:

部分主色的相对对比度不满足4.5:1的标准。

比如说,色号同样为酱紫与日出(黄色)两种颜色,黄色的对比度过低导致文本难以识别:

不过,在CSS Color Level 4定义了lch()函数,它是LCH的色彩空间。

LCH是一个比我们在CSS中熟悉的RGBHSL颜色都更具优势。在社区中,也有很多声音在传达:使用HCL色彩空间替代HSL(或HSB)色彩空间,也更建议使用HCL色彩空间来生成颜色系统。因为,HCL配色能更方便地达到高可用性(即颜色可访问性)要标准,同时对人眼更加友好。

HCL色彩空间中,当色相改变而L通道保持恒定时,对人眼而言,色彩对比度不变。另外从下面几点也可以更好的阐释HCL要比以往我们熟悉的HSBHSL更好:

  • HSL色彩空间中的明度L是相对于计算机元件而言,而非人类的眼睛。L通道与人眼对明度的感知(明度感知Luminance也常被称为相对亮度Relative Luminance,简写为Luma)非线性匹配,HSL中明度相等的两颜色,人眼感知到的明度可能相去甚远。但产品的使用者是人类,用色彩进行标识以人眼感知为准
  • HCL中只要两个颜色的L值(其实是Luma,即人类眼睛的感知明度)相等,颜色的对比度就相等
  • HCL对颜色识别有障碍的人士(比如视弱,色盲)更友好。对于有障碍的人士能通过颜色的对比度(指的是Luma)区分颜色的不同,对比度越强,颜色越容易区分。这一点也更能符合WCAG中颜色对比度的标准

其实,前面介绍颜色对比度计算中就已经提到了Luma,即感知明度,也就是HCL色彩空间对颜色的转换算法。换句话说,HCL在将来(或许就是现在)更能被设计成能够代表人类视觉的整个光谱,但并不是所有这些颜色都能在屏幕显示出来,哪怕是P3屏幕。根据屏幕的色域,不仅最大色度不同,实际上每种颜色的最大色度也不同。拿个例子来说,假设你的屏幕的色域与sRGB颜色空间完全匹配(如,2013年的MacBook Air的屏幕约为sRGB颜色空间的60%,尽管大多数现代屏幕约为sRGB150%)。对于L=50H=0(对应的是青色),最大色度只有35;对于L=50H=0(对应的是洋红色),色度可以达到77,也不会超过sRGB的界限;对于L=50H=320(对应的是紫色),它的色度可以达到108

虽然缺乏边界可能会有些令人不安(在人和色彩空间中),但不要过于担心:如果你指定了一个在给定监视器中不能显示的颜色,它将被缩小,以便在保持其本质的同时变得可见

对于不了解色彩的同学来说,要理解这里面的理论点是极其为难的。我就有这样的感觉,但在众多色彩空间中或者说色彩模式中来说,在未来HCL更符合人类的感知,用于颜色系统的构建也更为适合。只不过,HCL颜色的算法更为复杂。特别是,别的颜色模式转换为HCL时,就会涉及到很多数学公式。比如,W3C规范中提供了相应的转换函数

这里虽然没有向大家演示如何使用HCL色彩空间来生成颜色系统,但给大家提供@LeaVerou设计的一个HCL拾色器工具

有一点我们应该认识到,不管采用哪种色彩空间(或颜色模式)要构建一个完全具有可访问性的颜色系统都不是件易事,甚至说有点不可能,但我们在设计属于自己品牌颜色的系统时应该认识到:

  • 使用感知上一致的颜色模式:在设计可访问的颜色系统时,使用感知一致的颜色模型可以更好的帮助我们了解每种颜色在人类的眼睛中是如何呈现的,而不是在计算机显示器屏幕中是如何呈现的。这使我们能够验证我们的直觉,并使用数字来比较我们所有颜色的亮度和色彩
  • 可访问并不意味着充满活力:WCAG有关于可访问颜色标准有意识只关注**前景色(color背景颜色(background-color)**之间的对比,而不是它们看起来有多亮。了解每种颜色的鲜明程度有助于区分不同的色调
  • 颜色很难讲道理,工具可以帮忙:感知上一致的颜色模型的缺陷之一是不可能存在颜色。不存在“非常彩色的暗黄色”或“充满活力的淡黄色”之类的颜色。构建我们自己的工具帮助我们准确地查看哪些颜色是可访问的,并允许我们快速地迭代我们的调色板(品牌颜色系统),直到我们生成一个可访问的、充满活力的、仍然感觉像条纹的调色板

CSS如何控制颜色颜色对比度

CSS发展至今有了很大的变化,特别是近几年尤其突出。就拿CSS中的颜色模块来说吧,已经历经多个版本的变更,这在《CSS颜色》一文中已经可以领略到了。除了颜色模块自带的特性之外,还有其他的功能模块,比如CSS的自定义属性CSS的媒体查询等,都可以更好的帮助我们在开发时让颜色更具有可访问性。

接下来,我们就来简单的聊聊,在CSS中,我们如何借助其自身能力,让颜色变得更具可访问性。

在Web开发中,往往关注的是文本颜色(前景色)和背景色两者之间能够保持足够高的对比度,用来达到WCAG颜色可访问的标准。早期大多是借助于JavaScript相关的能力来实现。

function setForegroundColor(color) {
    let sep = color.indexOf(",") > -1 ? "," : " ";
    color = color.substr(4).split(")")[0].split(sep);
    const sum = Math.round(
        (parseInt(color[0]) * 299 +
        parseInt(color[1]) * 587 +
        parseInt(color[2]) * 114) /
        1000
    );
    return sum > 128 ? "black" : "white";
}

上面的代码只是用RGB颜色为例

setForegroundColor()函数将传入的color(一个RGB颜色)颜色的rgb通道的值乘以一些特殊的数字r * 299g * 587b * 144),将它们的和除以1000。如果得到的值大于128时,返回黑色,否则就会返回白色。

((R x 299) + (G x 587) + (B x 114)) / 1000

注意:这个算法是从RGB值转换为YIQ值的公式中得到的。此亮度值给出颜色的感知亮度(Luma)。

对于两个颜色的色差可以按下面的公式来计算:

(maximum (R1, R2) - minimum (R1, R2)) + (maximum (G1, G2) - minimum (G1, G2)) + (maximum (B1, B2) - minimum (B1, B2))

颜色亮度差的范围是125,色差的范围是500

这样一来,在改变元素背景色时,就可以自动匹配相应的前景色(主要是#000#fff二选一):

上面的按例使用了JavaScript来动态改变背景色的RGB通道值。但我们在实际开发中一般是在CSS中通过background-colorcolor来赋予元素的色彩:

.element {
    color: rgb(255 255 255);
    background-color: rgb(255 0 0)
}

早期的CSS在动态改变(比如说重新创建颜色值)值,也无法动态处理像if这样语句。幸运的是,CSS的自定义属性的出现让这件事情变得t简单地多,而且结合calc()函数,可以让我们在CSS中做一些简单地计算。这样一来,上面的公式,我们就可以使用calc()来完成:

calc((r * 299 + g * 587 + b * 144) / 1000) 

这个时候把上面公式中的rgb几个参数换成CSS自定义属性(因为CSS中并没有rgb这样的属性和值):

:root {
    --r: 255;
    --g: 0;
    --b: 0;
}

使用var()函数,将:root中声明的自定义属性替换公式中的rgb

calc((var(--r) * 299 + var(--g) * 587 + var(--b) * 144) / 1000) 

为了每次在使用的时候能少写一点代码,我们可以将上面的公式赋值给一个自定义属性,比如--a11yColor

:root {
    --r: 255;
    --g: 0;
    --b: 0;
    --a11yColor: calc((var(--r) * 299 + var(--g) * 587 + var(--b) * 144) / 1000) 
}

你可能已经发现了,在JavaScript版本中,我们将计算出来的值和128做了一个比较,然后才输出正确的值。那么问题来了,在CSS中怎么实现类似的功能呢?不要过于担心,我们同样借助CSS的自定义属性,可以实现类似于true1)和false0)这样的简单逻辑:

有关于CSS自定义属性如何实现简单的逻辑功能相关的介绍,可以阅读下面几篇文章:

我们继续回到颜色的计算中来。众所周知,RGB颜色模式的值是0 ~ 255(也可以是0% ~ 100%)之间,为了不让事情变得复杂化,这里以0 ~ 255为例。在使用rgb()函数来设置一个颜色时,它的值只能是在0 ~ 255的区间内,虽然规范上是这样定义的,但实际上取值小于0和大于255也是有效值,比如rgb(-255 300 220)是一个有效值,只不过浏览器将该值渲染为rgb(0 255 220)

从浏览器的渲染结果中我们不难发现:小于0时会取其下限值0,大于255时会取其上限值255。接下来,我们要处理的是“总值是否大于128”。在calc()函数的计算中是无法做比较的,我们只需要做的是从总和中减去128即可,从而得到一个正整数负整数。然后,如果我们将它乘以一个大的负值,比如-1000,将会得到一个非常大的正值或负值。最后把这些值传给rgb()函数:

:root {
    --r: 255;
    --g: 0;
    --b: 0;
    --a11yColor: calc((((var(--r) * 299 + var(--g) * 587 + var(--b) * 114) / 1000) - 128) * -1000);
}

.element {
    color: rgb(var(--a11yColor) var(--a11yColor) var(--a11yColor));
    background-color: rgb(var(--r) var(--g) var(--b))
}

效果如下:

我们可以基于这个原理,将上面的JavaScript计算的Demo换成CSS计算的方式:

前面我们提到过,不管是设计师还是开发人员,更喜欢使用HSL来定义颜色。同样的,我们除了JavaScript方案也可以类似于RGB一样,使用纯CSS的方案,实现颜色具有较好的可访问性。

RGB一样,同样可以将HSL和CSS自定义属性结合起来。使用HSL给背景色设置颜色值(同样使用CSS自定义属性来声明HSL)。这样做的好处是允许我们使用一种非常简单方法来确定颜色的亮度,并将其用于条件语句

:root {
    --h: 220;
    --s: 50;
    --l: 80;
}

.element {
    background-color: hsl(var(--h) calc(var(--s) * 1%) calc(var(--l) * 1%));
}

效果如下:

这里有一点需要特别提出,CSS中的HSLRGB类似,如果hls的值低于最低值(00%)会以0(或0%)计算,高于最高值(h360度,会以360计算,ls都会以100%)计算。换句话说,当hsl的值都为0时,颜色为黑色,当超过最高值时为白色:

因此,我们可以将颜色声明为HSL模式,从l(亮度)在数中减去所需的阈值,然后乘以100%以迫使它超过其中一个限制(低于0或高于100%)。因为我们需要负的结果以白色表示,正的结果以黑色表示,所以我们还需要将结果乘以-1

:root {
    --l: 80;
    --threshold: 60; /* 颜色亮度l的阈值被认为是0 ~ 100之间的整数,但建议采用50~70之间 */
}

.element {
    /* 任何低于阈值的亮度值将导致颜色为白色,反之为黑色 */
    --switch: calc((var(--l) - var(--threshold)) * -100%)
    color: hsl(0, 0%, var(--switch));
}

如果你对颜色稍微了解(或者多几次尝改上面自定义属性的值),不难发现,当一个元素的背景变得太亮时,它很容易在白色背景下不可见。为了在非常浅的颜色上提供更好的UI,可以基于相同的背景颜色上设置可见的边框(颜色更深一些)。这样的场景非常适合按钮一类的UI。

为了实现这个效果,我们可以使用相同的技术,但是要将它应用到HSLA颜色模式中的A(透明)通道。这样,我们可以根据需要调整颜色,然后选择完全透明或完全不透明。

:root {
    --h: 85;
    --s: 50;
    --l: 60;
    --border-threshold: 60;
    --threshold: 60
}

.element {
    --border-l: calc(var(--l) * 0.7%);
    --border-alpha: calc((var(--l) - var(--border-threshold)) * 10);
    --switch: calc((var(--l) - var(--threshold)) * -100%);
    color: hsl(0, 0%, var(--switch));
    border: 2vh solid hsla(var(--l), clac(var(--s) * 1%), var(--border-l), var(--border-alpha));
    background-color: hsl(var(--h) calc(var(--s) * 1%) calc(var(--l) * 1%));
}

同样的,基于HSL颜色模式脱离任何JavaScript库,同样能实现具有可访问性的颜色。比如下面这个Demo:

前面我们多次提到过感知亮度(Luma),在颜色空间中感知亮度和HSL颜色模式中的亮度L是不一样的。这样一来,我们可以基于RGB颜色模式,使用感知亮度来较正三原色,让颜色更能让人类的眼睛识别。到目前为止,计算感知亮度的公式有两种。第一种就是前面提到的(也是W3C规范中提供的):

L = (r * 0.299 + g * 0.587 + b * 0.144) / 255

如果用CSS的calc()函数来描述的话,像下面这样:

L = calc((var(--r) * 0.299 + var(--g) * 0.587 + var(--b) * 0.114) / 255)

另一个公式是由ITU提供的:

L = (r * 0.2126 + g * 0.7152 + b * 0.0722) / 255

同样的,CSS表述的话如下:

L = calc((var(--r) * 0.2126 + var(--g) * 0.7152 + var(--b) * 0.0722) / 255)

如果在颜色计算中引入感知亮度Luma的话,那我们就不能再基于HSL来描述颜色了,因为Luma的计算离不开RGB颜色各通道的值。

主要是在CSS中,我们很难将HSL颜色转换成RGB

下面我们简单看看引入Luma的颜色对比度计算是怎么使用。请直接看代码:

:root {
    /* 使用rgb模式来描述颜色,eg. rgb(255 0 0)*/
    --r: 255;
    --g: 0;
    --b: 0;

    /* 颜色亮度l的阈值(范围0~1),建议设置在0.5~0.5间 */
    --threshold: 0.55;

    /*深颜色边框的阈值(范围0~1),建议设置在0.8+*/
    --border-threshold: 0.8;
}

.element {
    background-color: rgb(var(--r) var(--g) var(--b));

    /* 使用sRGB Luma方法计算感知亮度Luma: 
        L = (r * 0.2126 + g * 0.7152 + b * 0.0722) / 255 
        L = calc((var(--r) * 0.2126 + var(--g) * 0.7152 + var(--b) * 0.0722) / 255)
    */
    --luma: calc((var(--r) * 0.2126 + var(--g) * 0.7152 + var(--b) * 0.0722) / 255);

    color: hsl(0 0% calc((var(--luma) - var(--threshold)) * -10000000%));

    /* 如果亮度高于边框阈值,则应用较暗的边框 */
    --border-alpha: calc((var(--luma) - var(--border-threshold)) * 100);

    border: 3vmin solid rgba(calc(var(--r) - 50), calc(var(--g) - 50), calc(var(--b) - 50), var(--border-alpha));  
}

改变--r--g--b就可以得到不一样的结果:

你可以尝试着拖动下面Demo的颜色滑块,查看效果:

随着技术不断的革新,CSS的特性越来越强大,很多特性都在服务于优化用户体验方面的。比如在CSS Color Adjustment Module Level 1CSS Color Module Level 5提到的新属性color-adjust属性和color-adjust()函数设置用户代理可以做什么来优化输出设备上元素的外观。默认情况下,考虑到输出设备的类型和功能,允许浏览器对元素的外观进行任何必要和谨慎的调整。就拿color-adjust属性来说吧,它有两值 **economy**和 exact。其中exact值可以告诉浏览器它不应该调整样式表中声明的颜色:

.card { 
    background-color: #98b3c7;
    border-bottom: 0.25rem solid #7c92a3;
    color: #f3f3f3;
    color-adjust: exact;
}

上面的示例,在.card设置了color-adjust值为exact。该声明会强制浏览应用在.card元素上的颜色应该尽可能的得到精确的渲染。根据用户设备的最佳能力,尽可能接近准确的值。

如果要是取值economy的话,将会告诉浏览器,让它根据自己认为为合适的颜色值进行调整。

如果你对color-adjust属性感兴趣的话,可以阅读@Eric Bailey的《The possibilities of the color-adjust property》一文。

在最新的媒体查询特性提供了更多优化用户体验的媒体查询特性,这些特性可以根据用户喜好设置来渲染页面。比如大家熟悉的prefers-color-scheme,可以让你在应用根据用户的设置来匹配正确的主题(苹果给其一个更为专业的词语:Dark Mode):

除此还有其他方面的特性,比如颜色反转,添加透明层,根据用户环境渲染等,这里就不多阐述,要是你对这方面感兴趣的话,可以阅读早前整理的《CSS媒体查询新特性》一文。

另外,在CSS中,除了颜色值可以决定一个元素的UI色彩,影响颜色对比度之外,还有其他CSS属性也会对设置好的颜色有影响,哪怕你设置的颜色是具有可访问性的,也会因为它们受影响。比如CSS中的opacitymix-blend-modefilter等。

当然,这些属性可能会影响Web的可访问性,但有的时候他们同样能帮助我们构建更具可访问性的颜色。比如,@Ana Tudor在她的文章《Methods for Contrasting Text Against Backgrounds》中就介绍了如何使用filtermix-blend-mode来帮助更复杂的背景环境下增强文本的可读性:

另外,@Robin Rendle在他的《Reverse Text Color Based on Background Color Automatically in CSS》文章中介绍了如何使用min-blend-mode和伪元素的结合实现文本根据背景颜色自动反转文本颜色。

这些都有利于帮助我们构建更具可访问性的颜色。也能更好的提高用户的阅读体验。

接下来要做什么取决于你

你可能已经发现了,色彩非常的丰富,因此也造成了色彩的复杂。

颜色的使用远比我们想象的要复杂,并不是天真的设置颜色值而以

同样的,在Web可访问性方面更是如此,甚至更为复杂。但我们并不能因为其复杂而止,我们更应该从此刻开始,让你的产品在颜色方面能达到WCAG的标准。将WCAG标准作为设计和开发需求的一部分。你甚至可以基于你的专业技能,为自己的产品创建最具可访问性的颜色品牌体系,这样你只要使用即可。如果你的专业知识还无法达到这个程度,你也可以考虑使用一些在线的工具,帮助你构建这个体系。

但话又说回来,即使你有一套完整的品牌颜色体系,我们也应该需要检测Web中颜色对比度,因为总是有的时候你使用的颜色并不能达到WCAG相关的标准,甚至可以说,有些场景即使达到了标准,也未必是最佳体验。这话怎么感觉有点悖论了,如果是的话,请回过头来查阅前面的**“WCAG标准的困惑”**一节中提到的内容。

最后我想说的是,我们不要停留在想的阶段,应该立刻去实践。只要动手去做了,你就有所获得。

总结

颜色是非常复杂的。它以无数种方式被传达和感知。虽然颜色对比度是一个简单的辅助来确定对比度,但这也是至关重要的。如果你想让你的应用或产品达到无障碍和包容性的设计,那这里就会超越仅仅陈述基本的颜色对比度比例。我们需要在我们的设计中传达所有颜色的复杂性,这样我们就可以满足不同人的视觉需求。

我并不是这方面的专业性人士,我只是对这方面感兴趣。文章中可能会有一些错误的结论或不对之处。如果你是这方面的专家,希望能拍正,或在下面的评论中留下您宝贵的建议和经验。最后,希望这篇文章能对你有所帮助。