Building High Contrast Mode Using Sass

Mark Mullan
Clover Platform Blog
10 min readJul 31, 2018
CC BY 2.0 Marco Verch

We’ve been hard at work building our new dashboard for Clover merchants: we completely scrapped our old Ember codebase in favor of React + TypeScript. Given this massive transition, there’s no shortage of work to be done, but for an unofficial 20% project, I decided to focus on accessibility and ADA compliance. Starting with a clean slate presented an opportunity to emphasize accessibility from conception.

When building web applications that emphasize accessibility, there are four major categories of disabilities that need to be accounted for:

  • Visual: Blindness, color blindness, and other visual impairments
  • Auditory: Profound, severe, moderate, and mild hearing loss
  • Motor: Cerebral palsy, Parkinson’s, muscular dystrophy, and other motor impairments
  • Cognitive: Down syndrome, autism, dyslexia, and other cognitive impairments

However, all users are, at one point or another in their lives, temporarily disabled. For example:

  • Visual: You’re in an environment with extremely high screen glare.
  • Auditory: You’re in a loud environment where your ability to hear is impaired.
  • Motor: You’ve broken your hand.
  • Cognitive: You’re suffering from prescription medication side effects, etc.

If you’ve ever been in a loud bar and relied on closed captioning to understand what’s happening on TV, you too have benefitted from accessibility features. Consciously thinking about building highly accessible products benefits everyone.

One aspect of building visually accessible products is using a high-contrast color palette — that is, ensuring that there is sufficient contrast between your foreground and background colors. The Web Content Accessibility Guidelines (WCAG) even quantify this requirement. However, these requirements sometimes conflict with today’s design trends (light gray text on a white background, white text on lighter colored buttons, etc.) that exist not only at Clover but throughout the industry. When building our web applications, we have two options:

  • Ensure that the default theme satisfies WCAG requirements, or
  • Satisfy these requirements using the Alternate Version clause.

We opted for the latter, and thus, High Contrast Mode was conceived. Many companies prefer this option, including Xbox and Twitter.

The list of requirements for High Contrast Mode was short and non-negotiable:

  • When High Contrast Mode is enabled, all gray text needs to darken past the threshold defined by WCAG.
  • When High Contrast Mode is enabled, our pale clover-green, clover-red, and clover-blue brand colors need to darken past that same threshold, both as text and as the background color of buttons.
  • All ad-hoc colors need to follow this same set of requirements.

Given these requirements, it seemed like a reasonable implementation would be:

  • When High Contrast Mode is enabled, we will apply a class name on one of the parent-most <div> tags shared by all web apps. When that class name is set, all children elements need to change their style appropriately.
  • When High Contrast Mode is not enabled, the default styles will be applied.
  • As a Clover developer, when I am adding new features, I do not want to have to remember all of these rules, and I do not want to be manually constructing different CSS selectors for regular mode vs. high contrast mode.

A Naive Gut Instinct

At first glance, this seemed like a straightforward problem to solve. After all, we’re using Sass, and we already have our color variables nicely defined in a config file:

$white: #fff;
$gray-5: #fbfbfb; // Background
$gray-10: #e0e0e0; // Border
$gray-20: #dee0e1;
$gray-30: #b1b6b8; // Disabled
$gray-50: #858d90; // Secondary
$gray-60: #76888c;
$gray-70: #344146;
$gray-80: #253338; // Text
$black: #000;
$clover-green: #43b02a; // Primary
$clover-blue: #059adb; // Secondary / Links
$clover-red: #ff5555; // Error
$clover-yellow: #edce3a; // Warning

So, if we just add a second set of colors to override the default values when we’re in high contrast mode, we’re golden!

.high-contrast-theme {
$white: #fff;
$gray-5: #617587; // Background
$gray-10: #617587; // Border
$gray-20: #617587;
$gray-30: #617587; // Disabled
$gray-50: #617587; // Secondary
$gray-60: #617587;
$gray-70: #344146;
$gray-80: #253338; // Text
$black: #000;
$clover-green: #328320; // Primary
$clover-blue: #0374A5; // Secondary / Links
$clover-red: #E60000; // Error
$clover-yellow: #edce3a; // Warning
}

But Sass doesn’t work like that. It does not have the concept of scoping the values of its variables inside of CSS selectors. And in retrospect, that makes sense — what kind of CSS would that Sass block compile to, anyway?

A Less-Naive Solution

When we broke down the problem, we realized that we need to generate CSS like this:

// what we want it to look like under normal circumstances
.my-element {
color: $gray-10;
}
// what we need it to look like when we are satisfying ADA
// compliance requirements
.high-contrast-theme {
.my-element {
color: $gray-70;
}
}

But no one wants to write all of that CSS manually every time. Ideally, you want Sass to help you generate all of that CSS from something that looks like this:

.my-element {
color: different-color-for-every-mode(gray-5);
}

How would we go about doing that? Sass functions are great for returning one computed value, but they cannot generate multiple blocks of CSS like we need. That’s where mixins come in. Then, if we organize our color variables by Sass map themes, we can generate both default and high-contrast-themed elements on the fly. For this approach, our color variables are organized like this:

$themes: (
default-theme: (
white: #fff,
gray-5: #fbfbfb, // Background
gray-10: #e0e0e0, // Border
gray-20: #dee0e1,
gray-30: #b1b6b8, // Disabled
gray-50: #858d90, // Secondary
gray-60: #76888c,
gray-70: #344146,
gray-80: #253338, // Text
black: #000,
blue-links: #059ADB,

clover-green: #43b02a, // Primary
clover-blue: #059adb, // Secondary / Links
clover-red: #ff5555, // Error
clover-yellow: #edce3a // Warning
),
high-contrast-theme: (
white: #fff,
gray-5: #617587,
gray-10: #617587,
gray-20: #617587,
gray-30: #617587,
gray-50: #617587,
gray-60: #617587,
gray-70: #344146,
gray-80: #253338, // Text
black: #000,
blue-links: #0374A5,

clover-green: #328320,
clover-blue: #0374A5,
clover-red: #E60000,
clover-yellow: #edce3a
)
);

And then we define a mixin like this:

@mixin colorify($color-key, $css-selector: &) {
@if ($css-selector == null) {
@warn "No parent css element found";
}
@each $theme-name, $theme-map in $themes {
@if map-has-key($theme-map, $color-key) {
@at-root {
:global(.#{$theme-name}) {
#{$css-selector} {
color: map-get($theme-map, $color-key);
}
}
}
} @else {
@warn "Key #{$color-key} not found in $themes defined in _colors.scss";
}
}
}

It uses the @at-root directive to magically jump out of where you nest it in your Sass to be at the top level. With this approach, when a developer is creating a new feature and wants to provide an ADA-compliant alternative color scheme, all they have to do is write a style like this:

.some-parent-div {
.another-outer-div {
.my-element {
@include colorify(gray-20);
}
}
}

The mixin produces the compiled CSS below. Everything after and including ____ represents the unique hashes that our css-loader module appends to CSS while compiling Sass. The hashes are not appended to the high-contrast-theme and default-theme classes because they are escaped with the :global selector. The child class names are added by defining the mixin to pass in a default value of & to the mixin’s argument list. It’s pretty neat that you can essentially use this technique in Sass to bind your context when invoking a mixin or function. The other hashes remain unique, which allows this implementation to keep the levels of selector specificity that we want.

.high-contrast-theme .some-parent-div___2837jv .another-outer-div____6462nv .my_element____1921qx {
color: #617587;
}
.default-theme .some-parent-div___2837jv .another-outer-div____6462nv .my_element____1921qx {
color: #dee0e1;
}

Pros:

  • This solution satisfies a lot of the requirements that it was meant to, and is easy for a new developer to learn.
  • Adding a new color scheme is trivial. Clover Halloween theme? Give me 10 minutes and it’ll be ready.

Cons:

  • The Sass mapping of themes is ugly, not DRY, and if we want to access just one variable, we basically need a getter function to keep our code clean and readable.
  • The mixin, as implemented, is incapable of doing inverse contrast ratios when High Contrast Mode is enabled (i.e., making a font lighter on a dark background).
  • The mixin is incapable of generating High Contrast Mode colors when a developer wants to use Sass’s tint and shade functions on a preexisting color variable.

After working with this implementation for a couple of days, it became clear that there had to be a better way. The cons were simply too significant to ignore.

A Better Solution (For Now)

The solution that we finally settled on was to just add a few additional accessible colors to the ones we already use.

$white: #fff;
$gray-5: #fbfbfb; // Background
$gray-10: #e0e0e0; // Border
$gray-20: #dee0e1;
$gray-30: #b1b6b8; // Disabled
$gray-50: #858d90; // Secondary
$gray-60: #76888c;
$gray-70: #344146;
$gray-80: #253338; // Text
$black: #000;
$clover-green: #43b02a; // Primary
$clover-blue: #059adb; // Secondary / Links
$clover-red: #ff5555; // Error
$clover-yellow: #edce3a; // Warning
// Accessibility colors
$accessible-gray: #617587;
$accessible-blue-links: #0374A5;
$accessible-clover-green: #328320;
$accessible-clover-blue: #0374A5;
$accessible-clover-red: #E60000;

For this approach, instead of our mixin looking up values in Sass maps, it just applies the styles based on two arguments that you pass to it.

@mixin make-accessible($default-value, $accessible-value, $css-property: color, $css-selector: &) {
@if ($css-selector == null) {
@warn "No parent css element found";
}
#{$css-property}: $default-value;@at-root {
:global(.accessible-theme) {
#{$css-selector} {
#{$css-property}: $accessible-value;
}
}
}
}

As a developer styling your elements both in default mode and high contrast mode, all you have to do is write code like:

.some-parent-div {
.another-outer-div {
.my-element {
@include make-accessible($gray-20, $accessible-gray);
}
}
}

And you’ll generate compiled CSS like:

.accessible-theme .some-parent-div___2837jv .another-outer-div____6462nv .my_element____1921qx {
color: #617587;
}
.some-parent-div___2837jv .another-outer-div____6462nv .my_element____1921qx {
color: #dee0e1;
}

Our mixin can also be used to change a property other than the color of an element:

.my-button {
@include make-accessible($clover-green, $accessible-clover-green, background-color);
}

How does this solution compare to the previous one that we ruled out?

Pros:

  • Color variables are DRY, and accessing one color variable deeply nested in a Sass map is no longer a problem.
  • Because the default-value and accessible-value variables are user supplied, this mixin is capable of allowing for inverse color ratios (i.e., it allows for colors to become either darker or lighter when High Contrast Mode is enabled).
  • This mixin can be used in conjunction with Sass functions like tint and shade to provide alternative accessibility colors when these functions are used.
  • This function is now generic enough to use with any CSS property. In the future, if we want our accessibility mode to also increase some elements’ font sizes to better account for visual impairments, we can continue to use the same mixin.

Cons:

  • Clover’s Halloween theme might take longer than 10 minutes to implement. :(

After applying this mixin a few hundred times, we’re left with the final result. Here’s our merchant dashboard with its default color palette:

And here’s our merchant dashboard with a subtle, ADA-compliant High Contrast Mode enabled 🎉:

Alternative Options

When trying to ensure that there is sufficient contrast between background and foreground colors, some companies just define a dark and a light font color, and implement a simple formula to determine which one to use. For example, in Wall Street Journal’s Squaire library, they implement a formula that converts a color hex code to an int, does some math, and figures out whether to render dark gray or white as the font color — those are the only two options. I can definitely appreciate the simplicity of a solution like that.

Another option is to generate an entirely different stylesheet for our default theme vs. High Contrast Mode theme, and serve up a different stylesheet depending on which theme the client is viewing the web application in. If our monolithic stylesheet grows too large, we may revisit this option in the future. Stay tuned for more.

Testing and Validation

How do you test and validate this kind of feature? For now, we’re relying on Khan Academy’s Tota11y tool to quickly ensure that we’re implementing new features with an ADA-compliant high-contrast color palette. In the future, we’ll need to integrate this functionality into an automated test, but it’ll take careful thought. After all, adding this kind of automated test could result in a ton of false positives, and we don’t want to slow down our speed of development. We still have deadlines, after all.

Do you have tips on implementing automated tests that help ensure an alternative color theme is implemented properly? I’m all ears.

Get in Touch?

Want to chat more about accessibility in tech? Let’s get in touch. Shoot me an email at mark.mullan@clover.com, and please ensure that the color contrast in your note meets ADA compliance.

High Contrast Mode will deploy to production around mid- to late-August.

--

--