Custom properties and the inheritance tree

castastrophe
6 min readJun 28, 2018

We’re in an exciting time right now for front-end developers full of exciting new features and improved ways to do the things that have always been hard about CSS. Want to create defined regions on a page and move elements into those regions without having to reassemble your DOM? No problem! Have some CSS Grid! Want to find out if this shiny new feature you want to use is supported by the browser it’s loaded in? Enjoy this convenient @supports query! Do you need a way to define a set of properties once and then call that value all over the place? I give you…

Custom properties

Custom properties, also known as CSS variables or cascading variables, are revolutionizing how we use CSS. For projects where the only Sass or Less functionality you are using is variables, you can start to walk away from preprocessors altogether and lean instead on this native CSS feature. If you already know what CSS variables are, I would skip ahead to the next section.

If you are used to the Sass or Less variable syntax, then these will look a little crufty. In truth, they’re not fully “variables” in the programming sense of the word; they do store values that can be referenced but they can’t easily be manipulated. The alternative name, cascading variables, gets a little closer to describing the goal of this new feature; namely, to provide a way to define properties in the traditional CSS-inheritance tree (more on that later). For more on why the team working on the spec went in this direction instead of the more simplistic $foo notation, check out this blog post from Daniel Glazman: CSS Variables, why we drop the $foo notation or this one by Tab Atkins, Jr.: Let’s Talk about CSS Variables.

How they work

CSS variables are case-sensitive so —-foo and —-FOO are separate properties. The syntax that is allowed for custom properties is very permissive and accepts anything except bad string- or url-tokens, unmatched parentheses, square brackets, or curly braces, and top-level semicolons or the bang [!] operator. The possibilities are endless. For example, this is a valid custom property: —-foo: if(x > 5) this.width = 10;

While this would not be useful as a variable, as it would be invalid in any normal property, it could potentially be read and acted upon with JavaScript at runtime. This means custom properties have the potential to unlock all kinds of interesting techniques not currently possible with today’s CSS preprocessors.

- CSS Variables: Why Should You Care? | Web | Google Developers

Here’s a link to the spec for you CSS-nerds who want to deep-dive on functionality: CSS Custom Properties for Cascading Variables Module Level 1

In practice

So how do we start to use these custom properties? We start by identifying all the re-usable properties in our project. A few examples of properties ripe for conversion include:

  • Color values for use on backgrounds, text, borders, etc.
  • Spacing, i.e., padding or margin values
  • Typography, such as font family and size

For spacing, I recommend picking a base unit and setting values relative to that unit. For example, if your base unit is 16px, you can multiply that by 2 or 4 to get card or band padding and divide it by 2 to get spacing between paragraphs or smaller padding between elements.

Let’s see it in action…

Introductory custom properties demo on CodePen: https://codepen.io/castastrophee/pen/oyQQmR

Fallbacks are very important in design systems where the custom property is defined in a different file than where you are calling it. Pick fallback colors for background color and text color that work well together, if possible, in case one or both fail.

You don’t necessarily have to define a property at the root either. You can define properties on a particular element or class name to provide tighter scope to those elements. Speaking of scope…

Inheritance

One of the great features of CSS is that styles cascade down the DOM allowing you to set properties on one level and have that value inherited on the levels below it. Custom properties are no different. When architecting your properties, there might be a need to overwrite values for a certain element or at a particular breakpoint such as adjusting your padding when the screen size falls below 768px or altering the background color for an alert card.

Inheritance example from the W3C spec (copied here for convenience):

If a custom property is declared multiple times, the standard cascade rules help resolve it. Variables always draw from the computed value of the associated custom property on the same element:

:root  { --color: blue;  }
div { --color: green; }
#alert { --color: red; }
* { color: var(--color); }<p>I inherited blue from the root element!</p>
<div>I got green set directly on me!</div>
<div id='alert'>
While I got red set directly on me!
<p>I’m red too, because of inheritance!</p>
</div>

Note that the actual color property is set just once and the custom property value is what is being changed at the different levels. Blue is the globally-scoped property but when you add a div, you pick up a more scoped value of green. Just like the normal specificity tree in CSS, if you then add an id value with a reassignment of the custom property, your styles will pick that up instead. This is incredibly powerful in a design system because it gives you the ability to define a set of global properties and then customize those for context.

Architecture

At the root level, we define variables that are relevant across the design system. For example, defining colors at the root makes them available to reference at any level. In addition to generic color variables, we can also define background and text colors along with a global spacing setting. These are baseline variables that can be extended, altered, and manipulated further down the DOM.

:root {
/* -- Define colors */
--color--red: #c00;
--color--black: #252527;
--color--gray: #d2d3d5;
--color--gray-dark: #252527;

/* -- Define properties */
--global--box--background-color: var(--color--gray);
--global--box--color: var(--color--black);
--global--spacing: 15px;
}‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍

When setting scoped variables, we have the option to extend global variables, set them locally, or manipulate a global variable using a function like calc().

/* -- Declare scoped variables */
.box {
/* Use fallbacks where you aren't sure if the variable will exist */
--box--background-color: var(--global--box--background-color, gray);
--box--color: var(--global--box--color, black);
--box--padding: var(--global--spacing);
/* Optionally create scoped variable for only box class */
--heading--color: #c00;
/* Custom properties updated by screen size */
@media screen and (min-width: 800px) {
--box--padding: calc(var(--global--spacing) * 2);
}
}‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍
Custom properties inheritance demo on CodePen at https://codepen.io/castastrophee/pen/pVYJRe/

Browser support

Braces for impact…

Support is actually not that bad. If you’ve dropped IE11, like we have for redhat.com, you’ll find that most modern browsers have custom property support.

As long as your fallback approach is focused on usability and not pixel-perfection, you can definitely get by with a pretty simple fallback. Before you set a property that references a custom property, set the fallback for IE11 and it’s friends.

:root {
--color-black: #252527;
--color-white: #fff;
}
.box {
color: black;
color: var(--color-black);
background-color: white;
background-color: var(--color-white);
}‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍

If you are setting background color with custom properties, for the sake of fallbacks, you really need to be setting the text color with them too. This ensures that if your background color fails, your text color fails with it and is still readable.

Coming soon!

I’m working on a follow-up post soon to dive deeper into architecture ideas and how to structure CSS variables in a large projects such that you can:

  • Reduce file size for your compiled CSS
  • Build a standard theme that can be easily customized
  • Combine Custom properties with your existing preprocessor

Resources

--

--

castastrophe

Design systems engineer at Adobe. Thoughts are my own.