Crane builds houses: small, medium and large

Custom Properties

A convenient way to store repeated values and use them in different parts of the code. Like variables in programming languages.

Time to read: 8 min

Let's assume we have a web page with many elements that have the same color text. The designer created a brand style, the guide was approved, and we began to layout. Our CSS might look something like this:

        
          
          .header-primary {  font-size: 2em;  color: #18191C;  margin-bottom: .5em;}.header-secondary {  font-size: 1.6em;  color: #18191C;}.text {  font-family: "Open Sans", sans-serif;  color: #18191C;  margin-top: 0;}.form-input {  font-size: 1em;  color: #18191C;  padding-top: 4px;  padding-bottom: 4px;}
          .header-primary {
  font-size: 2em;
  color: #18191C;
  margin-bottom: .5em;
}

.header-secondary {
  font-size: 1.6em;
  color: #18191C;
}

.text {
  font-family: "Open Sans", sans-serif;
  color: #18191C;
  margin-top: 0;
}

.form-input {
  font-size: 1em;
  color: #18191C;
  padding-top: 4px;
  padding-bottom: 4px;
}

        
        
          
        
      

The specific shade of black #18191C is used throughout the page in completely different elements: headers, text, buttons, input fields. It seems that this shouldn't cause any problems, but in fact, there are a number of inconveniences that we would like to avoid.

First of all, if tomorrow for some reason we need to change the shade of black a bit, we will have to do this in many places.

Secondly, we have to copy and paste HEX value of the color, or memorize the first few characters so that the text editor can suggest it to us.

All these inconveniences go away if we use CSS variables. They are often called custom properties.

What is it and how is it written?

A custom property is an arbitrary property with a specific value. It differs from a standard CSS property in the way it is written. To apply a custom property, you need to pass it to the CSS function var().

Standard property:

        
          
          .list-item {  margin-left: 10px;}.list-item .link {  margin-left: 10px;}
          .list-item {
  margin-left: 10px;
}

.list-item .link {
  margin-left: 10px;
}

        
        
          
        
      

Custom property:

        
          
          .list {  --element-gap: 10px;}.list-item {  margin-left: var(--element-gap);}.list-item .link {  margin-left: var(--element-gap);}
          .list {
  --element-gap: 10px;
}

.list-item {
  margin-left: var(--element-gap);
}

.list-item .link {
  margin-left: var(--element-gap);
}

        
        
          
        
      

Custom properties do not exist in the CSS specification. In terms of application methods, they are most similar to variables in programming languages. If we defined a custom property, we can reuse it as many times as we want thereafter.

Inheriting custom properties

Like regular inheritable properties (for example, font-size), custom properties are inherited down the tree. By defining a variable in a parent element, we can reuse it in any child element:

        
          
          <div class="cards">  <div class="card">    <h2 class="card-header">Tariff "Free"</h2>    <ul class="benefits">      <li class="benefits-item">1 user</li>      <li class="benefits-item">2 GB of traffic</li>    </ul>  </div>  <div class="card card--primary">    <h2 class="card-header">Tariff "Popular"</h2>    <ul class="benefits">      <li class="benefits-item">Up to 5 users</li>      <li class="benefits-item">20 GB of traffic</li>    </ul>  </div></div>
          <div class="cards">
  <div class="card">
    <h2 class="card-header">Tariff "Free"</h2>
    <ul class="benefits">
      <li class="benefits-item">1 user</li>
      <li class="benefits-item">2 GB of traffic</li>
    </ul>
  </div>
  <div class="card card--primary">
    <h2 class="card-header">Tariff "Popular"</h2>
    <ul class="benefits">
      <li class="benefits-item">Up to 5 users</li>
      <li class="benefits-item">20 GB of traffic</li>
    </ul>
  </div>
</div>

        
        
          
        
      
        
          
          .cards {  --main-color: #E6E6E6;}.card-header, .benefits-item {  color: var(--main-color);}.card--primary {  --main-color: black;}
          .cards {
  --main-color: #E6E6E6;
}

.card-header, .benefits-item {
  color: var(--main-color);
}

.card--primary {
  --main-color: black;
}

        
        
          
        
      

In this example, we redefine the variable's value for the card with the class .card--primary, and all child elements change color. This is where the main power of CSS variables lies. Change a value in one place, and it affects all places where the variable is used.

Open demo in the new window

But what if we don't know in advance what the nesting of elements will be? We can set a custom property on the root element of the page <html>, and then it will be guaranteed to be available in every element on the page. But usually, for these purposes, the pseudo-class :root is used, which is an alias for <html>:

        
          
          :root {  --gap-small: 10px;  --gap-medium: 20px;}
          :root {
  --gap-small: 10px;
  --gap-medium: 20px;
}

        
        
          
        
      

In the first example, the color #18191C is used in various elements of the page. We can assign this color to a custom property with a meaningful name and then use that property everywhere. It's very convenient, because it's easier to remember the name of the property than the HEX code of the color:

        
          
          :root {  --text-color: #18191C;}.header-primary {  font-size: 2em;  color: var(--text-color);  margin-bottom: .5em;}.header-secondary {  font-size: 1.6em;  color: var(--text-color);}.text {  font-family: "Open Sans", sans-serif;  color: var(--text-color);  margin-top: 0;}.form-input {  font-size: 1em;  color: var(--text-color);  padding-top: 4px;  padding-bottom: 4px;}
          :root {
  --text-color: #18191C;
}

.header-primary {
  font-size: 2em;
  color: var(--text-color);
  margin-bottom: .5em;
}

.header-secondary {
  font-size: 1.6em;
  color: var(--text-color);
}

.text {
  font-family: "Open Sans", sans-serif;
  color: var(--text-color);
  margin-top: 0;
}

.form-input {
  font-size: 1em;
  color: var(--text-color);
  padding-top: 4px;
  padding-bottom: 4px;
}

        
        
          
        
      

Note: Custom properties declared in :root can also be overridden if necessary:

        
          
          :root {  --main-color: #18191C;}.article-promo {  --main-color: #272822;}
          :root {
  --main-color: #18191C;
}

.article-promo {
  --main-color: #272822;
}

        
        
          
        
      

Fallback Values

If for some reason the value of the variable is not defined, we can pass a second parameter to the var() function, which will become the "fallback" value. Much like the font-family property. If the first value is not found, the browser will substitute the next:

        
          
          .section-title {  color: var(--primary-color, #222);}
          .section-title {
  color: var(--primary-color, #222);
}

        
        
          
        
      

A var() function can also be passed as a fallback value, which in turn can also have a fallback value:

        
          
          .section-title {  color: var(--primary-color, var(--black, #222));}
          .section-title {
  color: var(--primary-color, var(--black, #222));
}

        
        
          
        
      

Correctness of Usage

For standard CSS properties, the types of values are predefined, so the browser understands whether we are using the value correctly.

Using HEX color notation for the color property. This is correct.

        
          
          .valid-value {  color: #272822;}
          .valid-value {
  color: #272822;
}

        
        
          
        
      

Using a dimensional value for the color property. This is incorrect.

        
          
          .invalid-value {  color: 10px;}
          .invalid-value {
  color: 10px;
}

        
        
          
        
      

For custom properties, it’s different. Values are computed in the browser just before the page renders. The browser does not know in advance where a custom property will be applied, so by default, it considers any variable to be correct. Only after the value is substituted for the property does the browser know whether we applied it correctly. There are reasons for such behavior, let's consider an example:

        
          
          :root {  --big-header: 20px;}.promo-header {  color: var(--big-header);}
          :root {
  --big-header: 20px;
}

.promo-header {
  color: var(--big-header);
}

        
        
          
        
      

In this example, the browser substitutes the value of the variable --big-header (20px) as the value for the color property, but that doesn’t make sense. In such a situation, the browser does two things:

  • Checks whether the property is inheritable. If so, it looks for a value higher up the tree.
  • If the property is non-inheritable or the value is not found higher up the tree, it takes the initial default value (initial). For the color property of the header, this will be black.

Usage in JavaScript

In JavaScript, the values of custom properties are used in exactly the same way as standard CSS property values.

To get a value:

        
          
          element.style.getPropertyValue("--main-color")
          element.style.getPropertyValue("--main-color")

        
        
          
        
      

To set a value:

        
          
          element.style.setProperty("--translate", `${currentScroll}px`)
          element.style.setProperty("--translate", `${currentScroll}px`)

        
        
          
        
      

Hints

Custom properties can be used in any other functions. For example, in the calc() function.

        
          
          .logo {  display: inline-block;  width: calc(var(--size, 1) * 15px);  height: calc(var(--size, 1) * 15px);}.logo--small {  --size: 2;}.logo--medium {  --size: 3;}.logo--big {  --size: 4;}
          .logo {
  display: inline-block;
  width: calc(var(--size, 1) * 15px);
  height: calc(var(--size, 1) * 15px);
}

.logo--small {
  --size: 2;
}

.logo--medium {
  --size: 3;
}

.logo--big {
  --size: 4;
}

        
        
          
        
      

In this example, we set a default value of 1. If only the class .logo is used, the browser will set --size: 1. If one of the modifier classes is applied, the value of --size will be overridden, and the width and height of the element will be recalculated thanks to the use of calc().

And here’s how to apply a CSS variable in the linear-gradient function.

        
          
          .element {  --angle: 45deg;  background-image: linear-gradient(var(--angle), #235ad1, #23d1a8);}.element--inverted {  --angle: -45deg;}
          .element {
  --angle: 45deg;
  background-image: linear-gradient(var(--angle), #235ad1, #23d1a8);
}

.element--inverted {
  --angle: -45deg;
}

        
        
          
        
      

In practice