How To Use CSS Custom Properties To Write More Readable Code

Before being interrupted by the birthday of the United States, I was talking about CSS custom properties, which if you remember, allow you to create variables directly in CSS instead of having to use preprocessors.

CSS custom properties can do a few things preprocessed variables can’t because they aren’t compiled prior to reaching browsers. This allows custom properties to cascade as well as interact with the DOM/CSSOM dynamically.

In my previous post I showed you the syntax to define custom properties as well as how to use them via the var() function. What I didn’t offer was much in the way of practical examples and I want to spend the next few weeks offering some.

I’ll illustrate a few basic concepts and give you ideas for how you might use custom properties in practice and I’ll point you to more examples I found while researching this topic. I’ve divided the examples into the following concepts.

  • @supports
  • Human Readable Property Names
  • Contextual styling
  • Responsive: dynamic changes in @media queries
  • Non-Javascript dynamic changes
  • Javascript dynamic changes
  • Transitions/Transforms/Animations

I’ll show you the first few today and pick up again next week with examples involving @media queries.

Check for Browser Support with @supports

While custom properties have fairly good support, it isn’t universal. Global support for custom properties is over 70% as I write this, including from the latest version of all major browsers. However, 70% is still less than 100% and it’s a good idea to check to see that a browser can handle custom properties before using them.

You can use @supports (92% browser support with IE being the main holdout) to manually check for support and then write different code based on the result. If you’ve never worked with @supports before, you can follow this general pattern.

1
2
3
4
5
6
7
@supports ( (--variable: 0)) {
  /* supported */
}

@supports ( not (--variable: 0)) {
  /* not supported */
}

Here’s an example written in Sass. I defined a Sass variable $color and also a custom property –color on the :root selector. I assigned both a value of red.

The h1 selector tests for support of the custom property through the @supports rule. The condition tested is setting a value on a custom property. If it is supported, the color of the h1 uses the var() function to set the value of the custom property. If not, it uses the value after compile of the Sass $color variable.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$color: red;

:root {
  --color: red;
}

h1 {
  @supports ( --var: 0 ) {
    color: var(--color);
  }
  @supports ( not (--var: 0)) {
    color: $color;
  }
}

You may wonder why you’d do this and why not stick with the Sass variable since you know it will work in all cases. The reason is so the custom property can cascade and interact with the DOM where possible.

Custom Properties and Readability

Because they live directly in CSS, custom properties can help make your CSS more readable in ways that Sass variables can’t. Sass variables can make your Sass code more readable, but the readability is lost on compile.

Chris Coyier offered one way to do this. We often write a number of CSS properties using shorthand even though we could write out the individual properties separately. For example, the margin property.

1
2
3
4
5
6
margin: 1px 2px 3px 4px;

margin-top: 1px;
margin-right: 2px;
margin-bottom: 3px;
margin-left: 4px;

Most of the time the shorthand is what we want, but sometimes it’s more readable to write out the specific properties separately. There are, however, some properties that appear to be shorthand, but aren’t. For example, box-shadow.

1
2
3
.element {
  box-shadow: 2x 1x 8px 4px #444;
}

I don’t know about you, but I always have to look up what each of the values represents. I can easily figure out that the last value #444 is the color of the shadow, but I can never remember which of the other values are offsets or blur or whatever else is included in box-shadow.

To make your code easier to read you can create custom properties for the offsets, blur, spread, and color and then use var() to set the values of these properties on the box-shadow property.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
:root {
  --box-shadow-offset-x: 2px;
  --box-shadow-offset-y: 1px;
  --box-shadow-blur: 8px;
  --box-shadow-spread: 4px;
  --box-shadow-color: #444;
}

.element {
  box-shadow:
    var(--box-shadow-offset-x)
    var(--box-shadow-offset-y)
    var(--box-shadow-blur)
    var(--box-shadow-spread)
    var(--box-shadow-color);
}

It’s certainly more verbose, but it’s also easier to understand what each value controls in the box-shadow.

Another example of making your code more readable comes from Danny Markov writing for Tutorialzine and it involves using custom properties to create more readable names for CSS properties.

Sticking with box-shadow, I created three different shadows, a small, medium, and large and set them all as global variables on :root.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
:root{
 --small-shadow:  1px 1px 1px 1px rgba(0, 0, 0, 0.5);
 --medium-shadow: 3px 3px 3px 3px rgba(0, 0, 0, 0.5);
 --large-shadow:  5px 5px 5px 5px rgba(0, 0, 0, 0.5);
}

.element-s {
 box-shadow: var(--small-shadow);
}

.element-m {
 box-shadow: var(--medium-shadow);
}

.element-l {
 box-shadow: var(--large-shadow);
}

Each shadow can be used to set the appropriate box-shadow on a given element without having to look at the specific values to determine which shadow is which. In this example it’s easy to tell with a quick look at the values, but much of the time it won’t be so obvious.

Custom Properties and Contextual Styling

Phillip Walton brought up an interesting use for custom properties when he wrote about them a year and a half ago. While you may or may not think it’s a good idea to use contextual styling (styling an element based on where it appears in the DOM), he offered an example for using custom properties to achieve contextual styling in a way that’s better than most of us currently use.

Phillip uses buttons in his examples so I’ll work with something different, lists. I like using lists when I write and I use lists when creating navigation, which I’ll often style differently depending on whether it’s in the header, the footer, a sidebar, or wherever.

I’ll often set different margins and paddings on list items depending on where they’re located in the DOM and sometimes adjust font-size and font-weight. Because I create navigation from lists, I often float list items one way or another.

To create different looks for list items in different locations, I might have to code something like this, where I set a default list item style and then change values for lists inside a nav and again for lists inside nav inside the footer.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
li {
 margin: 0;
 padding: 0.5em 0;
 font-size: 1.25em;
 font-style: italic;
}

nav li {
 margin: 1em 0 0 0;
 padding: 0.75em 1em;
 font-size: 1.5em;
 font-style: normal;
}

footer nav li {
 margin: 1em;
 padding: 0.25em 0.5em;
 font-size: 0.85em;
 font-style: italic;
}

This tightly couples structure and presentation and as I might need more lists in different locations, I could be heading for a specificity nightmare. Custom properties can be used instead.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
.list-item {
 margin: var(--li-margin: 0);
 margin: var(--li-padding: 0.5em 0);
 margin: var(--li-font-size: 1.25em);
 margin: var(--li-font-style: italic);
}

.nav-item {
 margin: var(--li-margin,1em 0 0 0);
 padding: var(--li-padding, 0.75em 1em);
 font-size: var(--li-font-size, 1.5em);
 font-style: var(--li-font-style, normal);
}

.footer-item {
 margin: var(--li-margin,1em);
 padding: var(--li-padding, 0.25em 0.5em);
 font-size: var(--li-font-size, 0.85em);
 font-style: var(--li-font-style, italic);
}

Again, it’s more code and it might not seem like much of a difference, but with custom properties, the components are unaware of their context. .footer-item doesn’t care about the values set in .nav-item and vice versa. Neither has to worry about overwriting the other or becoming too specific to override.

Note that I didn’t actually define the custom properties anywhere other than inside the var() function.

If I later want to add a list to a sidebar and style it in a different way, I don’t have to be concerned with how the list items are styled in footer or the main navigation. I can even set the list items back to the default easily.

1
2
3
4
5
6
.footer-item {
 --li-margin: initial;
 --li-padding: initial;
 --li-font-size: initial;
 --li-font-style: initial;
}

In the original example, the descendent selectors say lists inside the nav element will look a certain way, but lists inside a nav element inside a footer element will look another way. This couples the styles and structure and can make one set of styles difficult to overwrite when there’s an exception. It’s one way you end up having to create more specific selectors or end up using !important.

In the second example using custom properties, the CSS presentation isn’t coupled to the HTML structure. The selectors (.nav-item and .footer-item) are unaware of their context within the document and they can more easily be overwritten should there be a need for an exception.

Closing Thoughts

These weren’t the most complex examples and it’s possible you’re not too concerned about using custom properties for readability or contextual styling, but it’s probably a good idea to use @supports until browser support is closer to 100%

Regardless, we’re just getting started and I wanted to get a few simple examples out of the way. I said one of the main reasons to use custom properties is because you can change them dynamically.

Next week I want to look at some examples that use custom properties inside @media queries. It’s something you can’t do with preprocessed variables, but you can do with custom properties.

Download a free sample from my book, Design Fundamentals.

2 comments

  1. I’m confused by the second example. It shows the benefit of @supports but seems convoluted. As CSS is fault tolerant, @supports isn’t needed.

    $color: red;

    :root {–color:blue;}

    h1 {
    color: $color;
    color: var(–color);
    }

    The Sass preprocessor will replace $color with red. If the browser supports custom properties it overrides this with blue. If not, red prevails.

    • True. It’s probably not the best example in the world. I was just trying to give an idea of the syntax for using @supports. The example wasn’t meant to say do things this way. My bad for not coming up with a better one.

Leave a Reply

Your email address will not be published. Required fields are marked *