使用 Sass 创建可访问的颜色组合

Avatar of Jason Hogue
Jason Hogue

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 200 美元的免费额度!

我们都在寻找简单易行的途径,使我们的网站和应用程序更易于访问。我们可以做的一件比较容易的事情是确保我们使用的颜色赏心悦目。高颜色对比度对每个人都有益。它不仅可以减少一般的眼睛疲劳,而且对于视力下降的人来说至关重要。

因此,让我们不仅在设计中使用更好的颜色组合,还要找到一种方法使我们更容易实现高对比度。在 Oomph,我们有一个特定的策略,让一个 Sass 函数为我们完成所有繁重的工作。我将向您介绍我们是如何将它组合在一起的。

您是否已经了解了有关颜色可访问性的所有知识,并希望直接跳转到代码?请点击此处。

“可访问的颜色组合”的含义

颜色对比度也是我们可能认为已经处理过的事情之一。但是,高颜色对比度不仅仅是肉眼观察设计。WCAG 定义了不同的可接受标准级别,这些级别被认为是可访问的。打开 WebAIM 对比度检查器 并运行网站的颜色组合,实际上会让人感到谦卑。

我的团队默认遵循 WCAG 的 AA 级别指南。这意味着:

  • 24px 或更大的文本,或者如果加粗则为 19px 或更大的文本,应具有 3.0:1 的颜色对比度比 (CCR)。
  • 小于 24px 的文本应具有 4.5:1 的 CCR。

如果某个网站需要遵守 AAA 级别增强指南,则要求会更高一些

  • 24px 或更大的文本,或者如果加粗则为 19px 或更大的文本,应具有 4.5:1 的 CCR。
  • 小于 24px 的文本应具有 7:1 的 CCR。

比率? 嗯?是的,这里涉及一些数学运算。但好消息是,我们不需要自己动手去做,甚至不需要完全理解它们是如何计算的,就像 Stacie Arellano 最近分享的那样(如果您对颜色可访问性的科学感兴趣,这篇文章是必读的)。

这就是 Sass 发挥作用的地方。我们可以利用它来运行复杂的数学计算,否则这些计算会让我们很多人摸不着头脑。但首先,我认为在设计层面处理可访问的颜色是值得的。

可访问的颜色调色板始于设计

没错。创建可访问颜色调色板的核心工作始于设计。理想情况下,任何网页设计都应该参考工具来验证任何使用的颜色组合是否通过既定的指南,然后调整不符合指南的颜色。当我们的设计团队执行此操作时,他们会使用 我们内部开发的工具。它适用于颜色列表,在深色和浅色上测试这些颜色,并提供测试其他组合的方法。

ColorCube 提供了整个颜色调色板的概述,显示了每种颜色与白色、黑色甚至彼此配对时的性能。它甚至在每个结果旁边显示了 WCAG AA 和 AAA 级别结果。该工具旨在在评估颜色列表时一次向用户展示大量信息。

这是我们团队首先要做的事情。我敢断言,许多品牌颜色在选择时并没有将可访问性放在首位。我经常发现,当这些颜色转换为网页设计时,需要进行更改。通过教育、对话和视觉样本,我们让客户签署新的颜色调色板。我承认:这部分可能比实际实施可访问的颜色组合的工作更难。

颜色对比度审核:在使用现有品牌颜色调色板时,这是一个典型设计交付。在这里,我们建议停止使用品牌颜色翡翠色与白色搭配,而是使用稍微暗一些的“替代”版本。

我想通过自动化解决的问题是**边缘情况**。您不能责怪设计师错过了一些颜色以意外方式组合的实例——这种情况确实会发生。而这些边缘情况确实会出现,无论是在构建过程中还是一年后向系统添加新颜色时。

在保持颜色系统意图的同时开发可访问性

更改颜色以满足可访问性要求时的技巧是不要过多地更改颜色,以至于它们看起来不再是相同的颜色。一个喜欢其翡翠绿色的品牌会希望保持该颜色的**意图**——它的“翡翠感”。为了使其在用作白色背景上的文本时通过可访问性测试,我们可能需要使绿色变暗并增加其饱和度。但我们仍然希望颜色“读取”与原始颜色相同。

为了实现这一点,我们使用色相饱和度亮度 (HSL) 颜色模型。HSL 使我们能够保持色相不变,但调整饱和度(即增加或减少颜色)和亮度(即添加更多黑色或更多白色)。色相使绿色成为**那个**绿色,或使蓝色成为**那个**蓝色。它是颜色的“灵魂”,让我们对此进行一些神秘的思考。

色相表示为一个颜色轮,其值介于 0° 和 360° 之间——黄色在 60°,绿色在 120°,青色在 180° 等。饱和度是介于 0%(无饱和度)和 100%(完全饱和度)之间的百分比。亮度也是一个从 0% 到 100% 的值,其中无亮度为 0%,无黑色和无白色为 50%,100% 为全部亮度,或非常亮。

我们工具中调整颜色的快速视觉效果

使用 HSL,将低对比度绿色更改为高对比度意味着将左侧的饱和度从 63 更改为 95,将亮度从 45 更改为 26。当与白色一起使用时,颜色在中间获得绿色复选标记。但是,新的绿色仍然感觉属于同一个系列,因为色相保持在 136,这就像颜色的“灵魂”。

要了解更多信息,请使用有趣的 HSL 可视化工具 mothereffinghsl.com 进行尝试。但是,有关色盲、WCAG 颜色对比度级别和 HSL 颜色空间的更深入描述,我们撰写了一篇深入的博文

我想解决的用例

设计师可以使用我们刚刚回顾的工具调整颜色,但到目前为止,我还没有发现任何 Sass 可以用数学魔法做到这一点。**必须**有一种方法。

这些是我在实践中看到的一些类似方法

我不喜欢这些方法。我不想回退到白色或黑色。我希望颜色能够保持不变,但要调整为可访问的。此外,将颜色更改为其 RGB 或 HSL 分量并使用 CSS 变量存储它们看起来很混乱,并且对于大型代码库来说是不可持续的。

我想使用像 Sass 这样的预处理器来做到这一点:**给定两种颜色,自动调整其中一种颜色,以便这对颜色获得通过 WCAG 等级的分数。**规则还规定了一些其他需要考虑的事项——文本的大小以及字体是否加粗。解决方案必须考虑到这一点。

在代码方面,我想这样做

// Transform this non-passing color pair:
.example {
  background-color: #444;
  color: #0094c2; // a 2.79 contrast ratio when AA requires 4.5
  font-size: 1.25rem;
  font-weight: normal;
}


// To this passing color pair:
.example {
  background-color: #444;
  color: #00c0fc; // a 4.61 contrast ratio
  font-size: 1.25rem;
  font-weight: normal;
}

这样的解决方案能够捕获并处理我们之前提到的那些边缘情况。也许设计师考虑过将品牌蓝色用于浅蓝色之上,但没有考虑浅灰色。也许错误消息中使用的红色需要针对具有单一背景色的表单进行调整。也许我们想向 UI 实现暗模式功能,而无需再次测试所有颜色。这些是我想到的用例。

公式可以带来自动化

W3C 已 向社区提供了公式,有助于分析一起使用的两种颜色。该公式将两种颜色的 RGB 通道乘以神奇数字(基于人类如何感知这些颜色通道的视觉权重),然后将它们相除,得出一个介于 0.0(无对比度)和 21.0(所有对比度,仅限于白色和黑色)之间的比率。虽然不完美,但这是我们目前使用的公式

If L1 is the relative luminance of a first color 
And L2 is the relative luminance of a second color, then
- Color Contrast Ratio = (L1 + 0.05) / (L2 + 0.05)
Where
- L = 0.2126 * R + 0.7152 * G + 0.0722 * B
And
- if R sRGB <= 0.03928 then R = R sRGB /12.92 else R = ((R sRGB +0.055)/1.055) ^ 2.4
- if G sRGB <= 0.03928 then G = G sRGB /12.92 else G = ((G sRGB +0.055)/1.055) ^ 2.4
- if B sRGB <= 0.03928 then B = B sRGB /12.92 else B = ((B sRGB +0.055)/1.055) ^ 2.4
And
- R sRGB = R 8bit /255
- G sRGB = G 8bit /255
- B sRGB = B 8bit /255

虽然公式看起来很复杂,但这不就是一个数学问题吗?等等,没那么简单。在几行代码的末尾,有一个部分将值乘以一个小数幂——*提高到 2.4 次方*。注意到了吗?事实证明,这是一个复杂的数学运算,大多数编程语言都能完成——比如 Javascript 的 `math.pow()` 函数——但 Sass 的功能不够强大,无法做到这一点。

一定有其他方法……

当然有。只是花了一些时间才找到它。🙂

我的第一个版本使用了一系列复杂的数学计算,在 Sass 能力范围内完成了小数幂的运算。通过大量的谷歌搜索,我发现了一些比我聪明得多的人提供了这些函数。不幸的是,仅仅计算少数几种颜色对比组合,就使 Sass 构建时间呈指数级增长。所以,这意味着 Sass 可以做到,但这并不意味着它*应该*这样做。在生产环境中,大型代码库的构建时间可能会增加到几分钟。这是不可接受的。

经过更多的谷歌搜索,我偶然发现了一篇来自试图做类似事情的人的帖子。他们也遇到了 Sass 中缺乏指数支持的问题。他们想探索“使用牛顿近似法计算指数的小数部分的可能性”。我完全理解这种冲动(不是)相反,他们决定使用“查找表”。这是一个天才的解决方案。查找表预先计算了所有可能的答案,而不是每次都从头开始计算。Sass 函数从列表中检索答案,然后就完成了。

用他们的话说

唯一涉及指数运算的部分[Sass 中的部分]是在亮度计算过程中作为颜色空间转换的一部分进行的。[T]每个通道只有 256 个可能的值。这意味着我们可以轻松地创建一个查找表。

现在我们开始行动了。我找到了一个性能更好的方向。

使用示例

使用该函数应该很容易且灵活。给定一组两种颜色,调整第一种颜色,使其在与第二种颜色一起使用时通过给定 WCAG 等级的正确对比度值。可选参数还会考虑字体大小或粗细。

// @function a11y-color(
//   $color-to-adjust,
//   $color-that-will-stay-the-same,
//   $wcag-level: 'AA',
//   $font-size: 16,
//   $bold: false
// );


// Sass sample usage declaring only what is required
.example {
  background-color: #444;
  color: a11y-color(#0094c2, #444); // a 2.79 contrast ratio when AA requires 4.5 for small text that is not bold
}


// Compiled CSS results:
.example {
  background-color: #444;
  color: #00c0fc; // which is a 4.61 contrast ratio
}

我使用函数而不是混入,因为我更喜欢单个值独立于 CSS 规则的输出。使用函数,作者可以确定应该更改哪种颜色。

一个包含更多参数的示例如下所示

// Sass
.example-2 {
  background-color: a11y-color(#0094c2, #f0f0f0, 'AAA', 1.25rem, true); // a 3.06 contrast ratio when AAA requires 4.5 for text 19px or larger that is also bold
  color: #f0f0f0;
  font-size: 1.25rem;
  font-weight: bold;
}


// Compiled CSS results:
.example-2 {
  background-color: #087597; // a 4.6 contrast ratio
  color: #f0f0f0;
  font-size: 1.25rem;
  font-weight: bold;
}

深入了解 Sass 函数的核心

为了解释这种方法,让我们逐行了解最终函数在做什么。在此过程中有很多辅助函数,但核心函数中的注释和逻辑解释了这种方法

// Expected:
// $fg as a color that will change
// $bg as a color that will be static and not change
// Optional:
// $level, default 'AA'. 'AAA' also accepted
// $size, default 16. PX expected, EM and REM allowed
// $bold, boolean, default false. Whether or not the font is currently bold
//
@function a11y-color($fg, $bg, $level: 'AA', $size: 16, $bold: false) {
  // Helper: make sure the font size value is acceptable
  $font-size: validate-font-size($size);
  // Helper: With the level, font size, and bold boolean, return the proper target ratio. 3.0, 4.5, or 7.0 results expected
  $ratio: get-ratio($level, $font-size, $bold);
  // Calculate the first contrast ratio of the given pair
  $original-contrast: color-contrast($fg, $bg);
  
  @if $original-contrast >= $ratio {
    // If we pass the ratio already, return the original color
    @return $fg;
  } @else {
    // Doesn't pass. Time to get to work
    // Should the color be lightened or darkened?
    // Helper: Single color input, 'light' or 'dark' as output
    $fg-lod: light-or-dark($fg);
    $bg-lod: light-or-dark($bg);


    // Set a "step" value to lighten or darken a color
    // Note: Higher percentage steps means faster compile time, but we might overstep the required threshold too far with something higher than 5%
    $step: 2%;
    
    // Run through some cases where we want to darken, or use a negative step value
    @if $fg-lod == 'light' and $bg-lod == 'light' {
      // Both are light colors, darken the fg (make the step value negative)
      $step: - $step;
    } @else if $fg-lod == 'dark' and $bg-lod == 'light' {
      // bg is light, fg is dark but does not pass, darken more
      $step: - $step;
    }
    // Keeping the rest of the logic here, but our default values do not change, so this logic is not needed
    //@else if $fg-lod == 'light' and $bg-lod == 'dark' {
    //  // bg is dark, fg is light but does not pass, lighten further
    //  $step: $step;
    //} @else if $fg-lod == 'dark' and $bg-lod == 'dark' {
    //  // Both are dark, so lighten the fg
    //  $step: $step;
    //}
    
    // The magic happens here
    // Loop through with a @while statement until the color combination passes our required ratio. Scale the color by our step value until the expression is false
    // This might loop 100 times or more depending on the colors
    @while color-contrast($fg, $bg) < $ratio {
      // Moving the lightness is most effective, but also moving the saturation by a little bit is nice and helps maintain the "power" of the color
      $fg: scale-color($fg, $lightness: $step, $saturation: $step/2);
    }
    @return $fg;
  }
}

最终的 Sass 文件

这是整套函数!在 CodePen 中打开它,编辑文件顶部的颜色变量,并查看 Sass 进行的调整

所有辅助函数以及 256 行查找表都在这里。大量的注释应该可以帮助大家理解正在发生的事情。

在遇到边缘情况时,我在开发过程中使用 SassMeister 中带有调试输出的版本非常有用,可以查看可能发生的情况。(我将主函数更改为混入,以便调试输出。)也可以随意查看此内容。

在 SassMeister 上试用此 gist。

最后,这些函数已从 CodePen 中剥离并放入 GitHub 存储库中。如果您遇到问题,请将其提交到队列中。

很棒的代码!但是我可以在生产环境中使用它吗?

也许吧。

我想说可以,但我已经在这个棘手的问题上迭代了一段时间了。我对这段代码很有信心,但希望能得到更多反馈。在一个小项目中使用它,并进行测试。告诉我构建时间如何。如果您遇到传递颜色值未提供的边缘情况,请告诉我。向 GutHub 存储库提交问题。根据您在其他代码中看到的代码提出改进建议。

我想说我已经*自动化了所有 A11y 方面的事情*,但我也知道它需要进行道路测试才能被称为 Production Ready™。我很高兴将其介绍给世界。感谢您的阅读,希望很快就能听到您如何使用它。