Gravity 3: Colour system

James Nash
Jun 19 · 12 min read

Gravity 3, the latest version of buildit’s design system, has completely revamped how we apply colours when styling UI elements. Our new system makes all UI components chameleons that can automatically blend in with the colour scheme of their surroundings on a page, and it does so purely with CSS. This article covers the motivations for making this and how we did it.

Ye olden days

Like many UI frameworks and design systems, older versions of Gravity defined a number of color purposes — for example default page foreground and background colours, colours for different kinds of interactive controls, and so on. We then assigned specific colour values to each of those purposes. In our case this was all expressed as SASS variables.

Screenshot of SASS code, showing a number of colour purpose variables being assigned colour values.
Screenshot of SASS code, showing a number of colour purpose variables being assigned colour values.
Some of Gravity v1’s colour purpose SASS variables

Elsewhere in our code, we would then reference those purpose variables when styling UI components. Here’s an example of how we did it for basic links:

The motivations for this approach were:

  • Using variable names that express intended purpose (or “semantic names”, if you prefer) encourages developers to use them for the same kinds of things. In the UI that ultimately manifests itself as more visual consistency. UI elements that have a similar purpose tend to also be styled with similar colours.
  • It adds a layer of abstraction between our “raw” colour values, which came from our brand colour palette, and how and where they get used in the UI. If our brand changes, we simply map new brand colour values to our colour purpose variables and downstream in the code nothing else needs to change.

To aid accessibility, we also chose our colour assignments such that certain purposes could be paired with the default page background or foreground colours and the resulting combination would have an acceptable colour contrast. As long as developers styling components used the colour purpose variables as intended, they could just trust the system to ensure accessible text and background colour pairings.

While this all worked pretty well, we did hit some limitations. The design of the buildit website, which is our design system’s flagship consumer, evolved and introduced footers and other page regions with different background colours from the default:

A partial screenshot of a webpage, showing 3 regions each containing text, but also each with differing background colours
A partial screenshot of a webpage, showing 3 regions each containing text, but also each with differing background colours

This meant we had to override the usual colours for UI components when they were within a region with a non-default background. Broadly speaking, there’s two ways people tend to go about this in CSS. Either they add variants of UI components, for example by means of BEM-style modifier classes like button--inverted, and page authors then need to add those in the HTML for anything within such a region. Or, you can use CSS descendent selectors to select certain components within a region. That takes the onus off whoever’s writing the HTML to do anything special, but doesn’t work well if you plan to nest coloured sections within other coloured sections. We chose this approach though because we didn’t have a need for such nesting and wanted to keep our HTML simple. Here’s an excerpt from our page footer component’s code to illustrate this:

  // Override links' colours within the footer
a[href]:link,
a[href]:visited {
color: $grav-co-neutral-dirty-snow-white;
}

a[href]:hover {
color: $grav-co-neutral-ashtray;
}

}

If an <a href="..."> element appears on the page, it has the standard colours. If it happens to reside somewhere within a page footer, its colours automatically change to suit.

However, as you can see from the above SASS excerpt, we were now ignoring our colour purpose variables and just using (named) brand colour values directly. This was because our purpose variables didn’t account for different background colour contexts. We could have added more purpose variables for different contexts, but that wouldn’t scale very well. What would have happened if we needed even more colour schemes? It would have rapidly gotten unwieldy and defeated the original purpose of having a concise set of descriptive, well-understood and widely used colour purposes.

If you look closely, there’s another issue with this. You can’t safely put any UI component into a page footer (or any other region with a non-default colour scheme) until we’ve added some suitable colour overrides for it. We’ve covered basic text and links, but what if we needed to put a button in there? The same would have been true if we had been using modifier classes instead.

This is a pattern I’ve seen in other design systems: Some components will have a handful of variants for situations where they are used on a non-default background. But other components don’t. Want to nest a card within a pink sidebar? Tough luck!

It also means every new component needs to have multiple variants designed and built, if it is to work in all contexts. Likewise, adding support for new background colour means revisiting all your components and adapting their colours as necessary.

This got us wondering — could there be a more elegant solution?

The C in CSS stands for cascade

A while ago I wrote a post titled “A Tale of Two Buttons”, where I showed how CSS’s cascade and custom properties could be used to cut down the amount of styling code that needs to be written and create “chameleon” buttons that automatically adapt their colour scheme based on where they are in the DOM.

The basic approach is that you don’t assign explicit colour values in your CSS, but instead either inherit or use the currentColor where you can, and use CSS custom properties for anything else. The values that currentColor and custom properties resolve to, can be set by parent elements. If you do this comprehensively, then all of your UI component’s colours are not set in the component itself but instead inherited from their parents.

CodePen demo showing a button with 1 set of CSS rules appearing in 4 different colour schemes. In each case, it is inheriting colour values from a CSS class set on its parent container. (This example is taken from “A Tale of Two Buttons”)

That means you decouple individual UI components from colour schemes that can be applied to them. In doing so, you eliminate the need to have per-colour scheme variants of each UI component. Furthermore, you can add or change colour schemes without needing to modify any UI components.

Great. But, how many different colours do we need to style any UI component? Text and anything that should share the same colour as text is already covered by currentColor, so we’ve got 1 for free. But links should visually stand out, so that’s another colour. They also have a few states — visited, hover and active — so that’s potentially an additional 3 colours. What about buttons, checkboxes, etc… do we need to define custom properties for each part and state of all of those? And then assign colours to each for every colour scheme we make?

While there’s a huge number of things you theoretically could colour differently in a UI, designers often choose to restrict themselves to a finite palette and apply the same colours to multiple things. That can then be reflected in the code by a set of CSS custom properties. Enough to achieve your desired UI look, but not so many as to become difficult to understand and use.

This is where every design system team needs to make its own, opinionated decisions. One team might decide the links share the same colour as buttons and the check mark on a checkbox. A different team might instead decide that links and buttons should use different colours, but perhaps their headings use the same colour as links.

Ultimately, each design system team needs to clearly understand the design’s intent and then express that in the code. This will require collaboration and dialogue between designers and developers. Insights from developers can lead to changes in the designs, just as much as updates to designs can lead to changes in the code!

Defining Gravity’s colour purposes

In parallel to exploring Gravity’s refreshed aesthetic in design tools via mock-ups, we began taking a closer look at the sets of colours used elsewhere for inspiration. We examined UI frameworks like Bootstrap, editor colour schemes like Solarized, and even MS PowerPoint themes. How many different colours did they permit and what were they being used for?

Screenshot of a spreadsheet comparing colour purposes from Solarized, Bootstrap, MS PowerPoint and Slack themes.
Screenshot of a spreadsheet comparing colour purposes from Solarized, Bootstrap, MS PowerPoint and Slack themes.
We started collating some of our findings in a Google Sheet

We also wanted to encourage accessible colour contrast ratios. Someone creating a new UI component only has a set of colour purposes to work with, each of which may have arbitrary colours assigned to it. However, they need to have some means of knowing which pairings would yield accessible contrast ratios. Our aim was therefore to incorporate some kind of clue into our naming scheme.

Initially we leant towards dividing colours purposes into “foreground” and “background” groups. There would be a default for each and then a set of additional foregrounds or backgrounds. Any foreground could be safely paired with the default background and vice-versa. We explored the concept in a series of CodePen prototypes, such as this one:

That naming scheme didn’t stand up to closer scrutiny though. Imagine a button with a solid background. This needs to visibly stand out compared to its surroundings, so you want good contrast between the page background and the button background. Now the text label within the button also needs to visibly contrast with the button’s background, so we might re-use the page background as the button’s foreground. It’s easy to see how it would quickly get confusing when using such names.

Image for post
Image for post

We did however like the idea of having groups of colour purposes with the constraint that colours from one group would always create accessible contrast ratios with colours from another group. UI components could be styled based on that rule, and colour schemes would need to follow that rule. In the end, we settled on the super generic names of: “Group A” and “Group B”.

After much prototyping and discussion, we ended up with a total of 18 colour purposes — 6 in group A and 12 in group B. You can read about them in the “colour system” section of our pattern library. In other words, Gravity UI components can be styled with up to 18 different colours, no more. So far this is meeting our needs. Of course, we may end up extending the set in the future if a strong need arises. However, our hope is this intentional constraint will also influence future UI component design. Adding a new colour purpose to our design system will increase its complexity and should only be done with caution — much like deciding whether to reuse an existing UI component or making a new one.

Any group A colour can be paired with any group B colour and yield at least a 3:1 contrast ratio, which is the WCAG AA requirement for large text. A subset of combos (those involving at least one of the “neutral” colour purposes) need to achieve a 4.5:1 ratio, which is AA standard for normal sized text. This is something we have automated tests for. When editing or adding colour schemes, the assignments of colour values to our 18 colour purposes are checked and any that don’t meet the required criteria will fail the tests. Therefore, we can guarantee that our UI will always have accessible contrast ratios, regardless of which colour scheme is being applied.

Image for post
Image for post
Some output from our automated colour scheme tests

Two of the colour purposes are also special:

  • Group A “neutral” is always set as the background-color of the page or container
  • Group B “neutral” is always set as the color of the page or container

The reasoning for this is as follows:

  • Group B “neutral” is intended to be the default text colour, so in many cases this can simply be inherited and does not need to be explicitly set on individual HTML elements or UI components.
  • When styling an element or component, you need to know which group the surrounding background is (which is now guaranteed to be group A). Therefore if your component needs to visually contrast with that, you know you must pick a group B colour for whichever part of it needs to be visible against the surrounding background, such as its border.

In the final CSS, we apply a default colour scheme to the whole page using the :root selector like so:

  --grav-co-grp-b-neutral: #404040;
--grav-co-grp-b-neutral-subtle: #4d4848;
--grav-co-grp-b-neutral-emphasis: #404040;
--grav-co-grp-b-control: #4d4848;
--grav-co-grp-b-control-alt: #6f2c91;
--grav-co-grp-b-control-emphasis: #007c6c;
--grav-co-grp-b-control-active: #005c50;
--grav-co-grp-b-control-disabled: #666;
--grav-co-grp-b-accent: #007c6c;
--grav-co-grp-b-accent-success: #4d4848;
--grav-co-grp-b-accent-attention: #4d4848;
--grav-co-grp-b-accent-danger: #4d4848;
background: var(--grav-co-grp-a-neutral);
color: var(--grav-co-grp-b-neutral);

}

We also define a number of utility classes that use the exact same structure. These can be applied to any container and will set the colour scheme for that container and any UI components within:

  --grav-co-grp-b-neutral: #ccc;
--grav-co-grp-b-neutral-subtle: #999;
--grav-co-grp-b-neutral-emphasis: #e6e6e6;
/* ... */
background: var(--grav-co-grp-a-neutral);
/* Setting color here is redundant, since
it is inherited from :root */
}

However, we still use SASS to author our CSS, so we create some functions and mixins to simplify repetitive tasks. One example is applying a colour to a CSS property. Although CSS custom properties are very widely supported nowadays, some older browsers — notably all Internet Explorers, up to and including IE11, do not support them. We therefore took the view that using multiple colour schemes on the same page was a progressive enhancement. In IE (or anything else lacking support for custom properties) the fallback would be to use a single colour scheme across the whole page. In CSS we can achieve this with the following pattern:

  /* Then override using custom prop */
color: var(--grav-co-grp-b-neutral);
}

You can see how typing that out over and over would be tedious, so we made a mixin that takes the desired colour purpose and CSS property to apply it to, and then generates those 2 declarations:

This mixin will also make it easier for us to remove the fallback code in the future, if we ever feel there’s no more value in continuing to support very old browsers.

The end result

Putting all the pieces together, Gravity now has a robust and extensible colour system that:

  • Is implemented in pure CSS
  • Decouples colour schemes from the application of colours to UI components (allowing new UI components and new colour schemes to be developed independently)
  • Is progressive enhancement from a simpler version of the UI
  • Ensures accessible colour contrast ratios throughout the UI, regardless of which colour scheme is in use
  • Allows different parts of a page to be coloured using different colour schemes
Image for post
Image for post
Animated diagram showing how UI components are colored using the group A & B purposes and the effect of different color schemes setting the values of those color purposes.

The video below demonstrates this in action. Recoloring parts of a page (or even the entire page) is as simple as adding a single class to the parent element!

Video of color scheme classes being applied to identical blocks of HTML and them, including all their child elements, applying that color scheme. (You can view the demo live on CodePen)

While there’s plenty of prior art of using CSS custom properties for colouring a UI, we do believe the grouping and associated accessibility constraints are unique to Gravity. If others have made similar things, we’d love to hear about it and compare notes. For everyone else, we hope this write-up provides some useful ideas and inspiration that you can use in your own work!

buildit

we enable + accelerate engineering transformation

James Nash

Written by

Design system aficionado. Classically trained webmaster. Slayer of pixels. | https://cirrus.twiddles.com/

buildit

buildit

We are a global engineering & co-innovation consultancy specializing in solving highly complex business problems. We leverage engineering best practices, tools, methodologies and new ways of working to deliver true business outcomes.

James Nash

Written by

Design system aficionado. Classically trained webmaster. Slayer of pixels. | https://cirrus.twiddles.com/

buildit

buildit

We are a global engineering & co-innovation consultancy specializing in solving highly complex business problems. We leverage engineering best practices, tools, methodologies and new ways of working to deliver true business outcomes.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch

Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore

Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store