Serg Hospodarets Blog

Serg Hospodarets blog

CSS custom properties (native variables) In-Depth Serg Hospodarets Blog

I thought to start from explaining the purpose of having variables in CSS but actually popularity of CSS pre/postprocessors already did that.

Couple examples:

  • color variables for branding
  • consistent components properties (layout, positioning etc.)
  • avoid code duplications

Yes, of course, you still can search and find/replace all you need manually in your codebase but it’s like not having variables in JS- pain. Having dynamic and scoped CSS variables provides even more abilities for your experiments and applications- you can read, set and update them on the fly! Also you can prevent code duplications around your codebase as recently Webkit guys did in their Web Inspector.

And finally you have an interface to easily pass data from CSS to JS (for example media breakpoint values).

Here is the short-list of features CSS properties have:

  • they are dynamic and can be changed at runtime
  • they are easily to read/write from JS
  • they inherit, cascade and have scopes

So let’s dive deeper what CSS properties are and how to use them.

The name

Idea started as CSS variables but then was extended and restructured to CSS custom properties.

And it makes sense as more accurate name would be really CSS properties as it shows their nature/syntax. Now we also have related CSS @apply rule which provides the idea of kinda “mixins”.

So the current name is CSS Custom Properties for Cascading Variables

CSS Variables currently have two forms::

variables, which are an association between an identifier and a value that can be used in place of any regular values, using the var() functional notation: var(--example-variable) returns the value of the --example-variable value.
custom properties, which are special properties of the form --* where * represents the variable name. These are used to define the value of a given variable: --example-variable: 20px; is a CSS declaration, using the custom --* property to set the value of the CSS variable --example-variable to 20px.

First CSS variable

You might be surprised but you already might know and use a CSS variable (looks like the first one)- currentColor which is not so well known but still usable and works in all browsers.

It also has a scope and can be redefined:

:root { color: red; }
div { border: 1px solid currentColor; } /* border-color is red */

If you add:

div {
   color: black;
}

border color would be black

CSS variables syntax

Set

You can declare a variable using --variable-name: variable-value; syntax (names are case-sensitive). As a value you can use colors, strings, values etc.:

:root{
 --main-color: #4d4e53;
 --main-bg: rgb(255, 255, 255);
 --logo-border-color: rebeccapurple;
 --header-height: 68px;
 --content-padding: 10px 20px;
 --base-line-height: 1.428571429;
 --transition-duration: .35s;
 --external-link: "external link";
 --margin-top: calc(2vh + 20px);
}

Syntax might look ugly but there were couple reasons for that. E.g. $var variables syntax would be proceeded by CSS preprocessor otherwise.

Usage

You can use variables in CSS values like: some-css-value: var(--variable-name [, declaration-value]);

p {
 margin: var(--p-margin, 0 0 10px);
}

In the above example 0 0 10px would be applied if --p-margin is not assigned. Such ability makes things more flexible- you can use some variable from a framework (where usually many of them are defined) but at the same time be ready to remove it anytime saving your functionality working.

Scope

As it’s mentioned in the module’s documentation title- custom properties follow usual CSS cascade rules.

To provide a global variable use the :root scope:

:root{
 --global-var: 'global';
}

If you want to make a variable visible only for some element/component [re]define it for that specific element:

<div class="block">
  My block is
  <div class="block__highlight">awesome</div>
</div>
.block {
  --block-font-size: 1rem;
  font-size: var(--block-font-size);
}

.block__highlight {
  --block-highlight-font-size: 1.5rem;
  font-size: var(--block-highlight-font-size);
}

Media queries also provide “scopes” for you:

@media screen and (min-width: 1025px) {
  :root {
    --screen-category: 'desktop';
  }
}

Another simple example of the scope would be pseudo-classes (e.g. :hover):

body {
  --bg: #f00;
  background-color: var(--bg);
  transition: background-color 1s;
}

body:hover {
  --bg: #ff0;
}

As custom properties are global, to avoid conflicts better to follow a common convention naming for your variables (even easier to follow such “scopes” using a BEM naming convention) e.g.:

:root {
  /* main (page-wide) variables */
  --main-color: #555;
  --main-bg: rgb(200, 200, 200);
  /* accordion variables */
  --accordion-bg: #f00;
  --accordion-font-size: 1.5rem;
  --accordion__item-bg: #ded;
}

body {
  color: var(--main-color);
  background-color: var(--main-bg);
  /*...*/
}

Reassign vars from others

It’s possible to use variables assigning another ones --variable-name: var(--another-variable-name);:

.block {
  --block-text: 'This is my block';
  --block-highlight-text: var(--block-text)' with highlight';
}

.block:before {
  content: var(--block-text);
}

.block__highlight:before {
  content: var(--block-highlight-text); /*This is my block with highlight*/
}

There is a problem here- you cannot easily calculate new variables using defined ones. But we have CSS calc() so we can use it instead:

.block {
  --block-font-size: 1rem;
}

.block__highlight {
  /* DOESN'T WORK */
  --block-highlight-font-size: --block-font-size)*1.5;
  font-size: var(--block-highlight-font-size);
  
  /* WORKS */
  font-size: calc(var(--block-font-size)*1.5);
}

Be careful with huge expressions as they might impact the app’s performance.

calc() for values

As it was already mentioned, you cannot simply use variables like:

    padding: var(--spacer)px

But you can use calc() for that and calculations. Let’s make a simple example of vertical rhythm:

    margin: 0 0 calc(var(--base-line-height, 0) * 1rem);

Finally you can always reset/inherit the value

By default CSS custom properties inherit. In the case when you want to minimise any side effects for your blocks/components you can simply reset custom properties:

.with-reset {
  --bgcolor: initial;/* RESETS VALUE */
  --color: green;/* CHANGES VALUE */
  --border: inherit;/* DOESN'T CHANGE ANYTHING, AS INHERITED BY DEFAULT */
}

Access custom properties from JavaScript

You can easily read/write custom properties from JS. For that use CSSStyleDeclaration Interface (getPropertyValue, setProperty):

// READ
const rootStyles = getComputedStyle(document.documentElement);
const varValue = rootStyles.getPropertyValue('--screen-category').trim();
// WRITE
document.documentElement.style.setProperty('--screen-category', value);

Here is a demo of using the Custom Property --screen-category which represents the current screen type and is allowed to be assigned from the UI:

In that demo is shown an easy way to debug custom properties. In JS:

// GET
alert(
    getComputedStyle(document.documentElement).getPropertyValue('--screen-category').trim();
);

// SET
document.documentElement.style.setProperty('--screen-category', 'custom');

// or reassign from an another prop
document.documentElement.style.setProperty(
    '--screen-category', 'var(--default-screen-category, '%DEFAULT VALUE IF VAR IS NOT SET%')'
);

Ability to assign anything as a CSS variable value and an easy interface to read/write that from JS, allows to skip the old hacky ways of passing data from CSS/Sass to JS (e.g. list of media queries breakpoints).

For debug you can just output the value of a variable on your page in the content rule:

body:after {
  content: '--screen-category : 'var(--screen-category);
}

Browser Support

Custom properties already work in stable Chrome, Firefox and desktop Safari 9.1:

They are under consideration in Microsoft Edge

There are some limitations and bugs:

Here are ways to TEST if CSS custom properties are supported in the browser. CSS:

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

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

JS:

if (window.CSS && window.CSS.supports && window.CSS.supports('--a', 0)) {
  alert('CSS properties are supported');
} else {
  alert('CSS properties are NOT supported');
}

For old browsers (without CSS.supports() API) you can use Wes Bos’ test.

Fallbacks / polyfills

There are couple examples of PostCSS plugins, but no plugin can achieve true complete parity according to the specification because of the DOM cascade unknowns

  • they are not dynamic.

It might be solved when we see the bright future and CSS Houdini group dream of implementing an easy native way for CSS “polyfills” to all major browsers will come true. And even in that case variables syntax, most of all, cannot be shimmed.

But so far here is the list:

Using together with CSS preprocessor (SCSS)

Same variable names

Other small suggestion to start using CSS custom properties now with preprocessors is usage of a mixed syntax checking the browser support:

@supports ( (--a: 0)) {
/* Custom properties are supported in the browser */
:root{
  --main-bg: #4d4e53;
}

body {
  background-color: var(--main-bg);
}
}

@supports ( not (--a: 0)) {
/* Custom properties are NOT supported in the browser */
$main-bg: #4d4e53;

body {
  background-color: $main-bg;
}
}

In such case both CSS and Sass variables are created but the Sass variable is used only if custom properties are not supported in the browser.

Or you can move such logic and hide it under the Sass mixin:

@mixin setVar($varName, $value){
  @include setVarSass($varName, $value);
  @include setVarCss($varName, $value);
}

@mixin setPropFromVar($propName, $varName){
  @supports ( (--a: 0)) {
    // Custom properties are supported in the browser
    #{$propName}: getVarCss($varName);
  }
  
  @supports ( not (--a: 0)) {    
    // Custom properties are NOT supported in the browser
    #{$propName}: getVarSass($varName);
  }
}

// SET
@include setVar('main-color', #f00);

// GET
body {
  @include setPropFromVar('color', 'main-color');
}

Global variables

The ideas of the variable scopes are different in Sass and CSS, but here is the common way of doing that:

/* SCSS */
$main-color: #f00 !global;

/* CSS */
:root{
    --main-color: #f00;
}

Assigning the variable only if it’s not assigned so far

A common case when you expect that variable might be already defined and want another value to be applied only if it’s not assigned:

/* SCSS */
$main-color: #f00 !default;

body{
    color: $main-color;
}

Unfortunately you cannot do it such easily in CSS:

/* CSS */
body{
    --main-color: var(--main-color, #f00); /* DOESN'T WORK */
}

But you can create a new variable:

/* CSS */
body{
    --local-main-color: var(--main-color, #f00); /* DOES WORK */
    color: var(--local-main-color);
}

Or do that directly during the usage:

/* CSS */
body{
    color: var(--main-color, #f00); /* DOES WORK */
}

Interesting usages

Custom properties provide a huge area of interesting ideas:

  • now you have a clear native way to make your CSS talk to JS without hacks we used to use

  • Another example is using custom properties for internationalization where external link text and colors are changed depending on the selected language

  • A curious Jake Archibald’s proposal to control elements visibility using CSS variables depending on which blocks and their styles are loaded in the page: article

  • Theme switching: now instead of adding CSS rules for a particular class or loading an additional file with CSS rules to change the site theme, you can use custom properties as described by Michael Scharnagl in the post.

  • I also have some ideas how to use them e.g. for domain-specific branding (to provide different look and feel for instance domain1.site.com and domain1.site.com). For that we can easily upload and apply an external CSS file (depending on a domain) which will redefine some custom properties set.

The last idea and demo is close to theme switching based on custom properties, so you can use it in both cases:

Demo

Demo

Inspired by wonderful Wes Bos demos of interacting with CSS custom properties I decided to step even further and calculate colors from R,G,B channels (defined by the user) in CSS using calc();.

For grayscale filter the code is:

.grayscale {
    background-color: rgba(
            calc(0.2126 * var(--r)),
            calc(0.7152 * var(--g)),
            calc(0.0722 * var(--b)),
            1
    );
}

Demo

Interesting facts:

  • Chrome doesn’t like multiplying/dividing with non integer numbers in calc() with CSS variables
  • Firefox doesn’t work with custom properties in calc() inside of rgba() at all
  • Demo works in Safari as expected 😊

Сonclusions

Now you know what CSS custom properties are and:

  • their syntax to interact from CSS and JS
  • they are dynamic, inherit, cascade and have scopes
  • browser support and fallbacks for them
  • they can be used in parallel with Sass variables
  • some interesting usages and examples where custom variables open absolutely new capabilities for developers and web platform in general

I hope after reading this article you are excited to start using custom properties.

Further reading

Recently CSS native mixins syntax was announced - read my article CSS @apply rule (native CSS mixins)

Provide your code in <pre><code>...</code></pre> tags in comments