CSS Variables


Published: 2018-06-22
Updated: 2018-06-23
Web: https://fritzthecat-blog.blogspot.com/2018/06/css-variables.html


When no one expects it any more, it happens: CSS got variables! They were called "Custom Properties", reason may be that their definitions look nearly the same as property definitions. The value of a such a custom property can then be received by a "CSS Variable".

Definition

A CSS custom property definition is written like a property definition, just that the property name is preceded by a "--":

body {
--border: 0.6em solid gray;
}

This custom property is defined in the global context of the HTML <body> element, thus it should be available everywhere. Its base name can be any name, even the name of an existing CSS property like "border". Mind that these names are case-sensitive. The body selector can be replaced by any possible CSS selector.

Would the custom property have been defined on <div>, then it would not be known in <body>, a custom property is bound to the context of its rule. Its value can change when a deeper element overwrites it, but going back upwards would restore the former value ("cascading variables"). The strength of a variable is bound to the specificity of its selector.

Usage

After we defined a custom property, we would like to use it as variable value:

article {
border: var(--border);
}

Here it was used as value for the actual CSS border property. The var() function provides the value of the custom property to the CSS border property. It doesn't insert the value, it just provides it. (I'll come back to this later.)

Mind that you can use variables just on the right side of a rule. Following attempt to generate the left-side property name margin-top from a variable will fail:

.foo {
--side: margin-top;
var(--side): 20px;
}

Also building together values doesn't work:

.foo {
--gap: 20;
margin-top: var(--gap)px;
}

This is an attempt to declare the pixel unit on the actual rule only, but it will fail.

On the other hand this will work:

input::before {
--label: 'This is my text';
--highlighted-label: var(--label)' highlighted';

content: var(--highlighted-label);
}

Here two texts are built together to "This is my text highlighted".

Asset?

The actual asset of a variable is avoiding copy & paste programming.

/* variable definition */
body {
--border: 0.6em solid gray;
}
/* variable usage */
header {
border: var(--border);
}
nav {
border: var(--border);
}
aside {
border: var(--border);
}
footer {
border: var(--border);
}

Here all rule-sets take advantage of the same variable. Changing the border in all these elements requires the change of just one line of code.

But is this really an asset? Look at this:

header, nav, aside, footer {
border: 0.6em solid gray;
}

Does the same in just one line of code!

Let's look deeper into it, and decide later whether "Custom Property" is a useful feature.

Calculations

By means of the CSS calc() function we can calculate values also with variables:

body {
--padding: 0.6em;
}
div {
padding: calc(var(--padding) - 0.4em);
}

The variable --padding is used to calculate a concrete padding for any <div> element. The result will be 0.2em.

Asset!

A variable gets updated any time its custom property changes, and all calculations done using variables also will be updated! This is something that no CSS preprocessor like Sass or Less provides, they just substitute text.

section    {
--big-font: 4em;

font-size: var(--big-font);
/* font-size is 4em */

--big-font: 3em;
/* now font-size has become 3em */
}

Redefining the custom property "--big-font" would immediately change the font-size CSS property. A CSS variable is a reference to a custom property, not a copy of its value.

This I would regard to be a real asset: reactive programming has reached CSS!

Variable Cascading

Borders are not inherited to sub-elements in CSS, thus they are well suited to demonstrate the cascading ability of variables. (With colors this would be not so obvious, because colors are cascaded to sub-elements by default.)

 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<!DOCTYPE HTML>
<html>
<head>
<style>
body {
--border: 0.6em solid blue;
}
main {
--border: 0.6em solid green;
}
p {
--border: 0.6em solid red;
}
* {
border: var(--border);
}
</style>
</head>

<body>
The BODY element has a blue border.

<div>
A blue border inherited from BODY would be around this DIV.
</div>

<main>
This MAIN would have a green border.
<p>
Here around this P would be a red border.
</p>

<div>
In this DIV, red has been reset to green from MAIN parent.
</div>

</main>

<div>
Green has been reset to blue in this DIV.
</div>

</body>

</html>

In the header's <style> element, the <body> element defines a custom property "--border" declaring a blue border, <main> redefining it to a green border, <p> to a red border. Mind that for <div> no custom property was defined.
Finally all elements (the * selector) are given a CSS border property that refers to the "--border" custom property (which has been defined three times).

The subsequent HTML then uses <div>, <main> and <p> in different combinations. It would look like the following:

The BODY element has a blue border.
A blue border inherited from BODY would be around this DIV.
This MAIN would have a green border.

Here around this P would be a red border.

In this DIV, red has been reset to green from MAIN parent.
Green has been reset to blue in this DIV.

As expected the <body> gets a blue border. The <div> sub-element of it has a blue border again, because every element was given a border, and the setting is cascaded from <body>. The nested <main> has a green border, because there is a CSS selector matching it, and the contained rule redefines the "--border" variable. Same with the further nested <p> element, which has a red border. After leaving the <p> the color jumps back to green, as we see on the <div> inside <main>, and after leaving <main> back to blue. Thus the last <div> element has a blue border again.

Fallbacks

The var() function provides place for a default value:

div {
--thick-border: 0.6em solid gray;
}
p {
--thin-border: 0.3em solid lightGray;
}
article {
border: var(--thick-border, var(--thin-border));
}
section {
border: var(--thick-border, 1px solid black);
}

The <article> rule evaluates to the variable "--thick-border", and, in case that does not exist, uses "--thin-border" as default. This would happen in case the <article> element was not wrapped into a <div> but into a <p>, because the "--thick-border" variable appears just in the context of a <div> element. In case it was wrapped in neither, it would have no border.

The <section> rule defines the concrete setting "1px solid black" as default for the case it was not wrapped into a <div> element.

Conclusion

Intuitively, as a reader and programmer, I do not like the -- prefix. It is too close to --i; which is for decrementing the numeric value of variable i in many C-like languages.

But, all in all, custom properties are worth being tried out. The auto-update feature makes it a great tool for reactive user interfaces.





ɔ⃝ Fritz Ritzberger, 2018-06-22