1
0 Comments

🤔 What no one told you about CSS Variables

CSS Variables are great but do you know everything about them?

In this post, I will highlight few quirks around CSS variables that no one talks about. After that, you won't look at them the same way anymore.


Table of content

  1. Be careful with !important
  2. They cannot store URLs
  3. They can make an invalid value valid
  4. They can be used unitless
  5. They cannot store the inherited value
  6. They only work from parent to child
  7. They can have strange syntaxes

1) Be careful with !important

Using !important with CSS variables is a bit tricky so let's start with a basic example:

p {
  --color:red!important;

  color:var(--color);
  color:blue;
}

What will be the color of p? you think it's red because we will have the following:

p {
  color:red!important;
  color:blue;
}

But it's not! the color of p will be blue because we will have the following:

p {
  color:red;
  color:blue;
}

!important in this case isn't part of the value of color but is used to increase the specificity of --color.

Note: Custom properties can contain a trailing !important, but this is automatically removed from the property’s value by the CSS parser, and makes the custom property "important" in the CSS cascade. In other words, the prohibition on top-level "!" characters does not prevent !important from being used, as the !important is removed before syntax checking happens.

Here is another example to better understand:

p{
  --color:red!important;
  --color:blue; 

  color:var(--color);
}

The above will give us a red color:

  1. We have two declarations of the same property called --color so we need to resolve the cascade. The first one is having !important so it wins
  2. We have our winner (--color:red!important) so !important is removed then the value is applied to color
  3. We have color:red.

Let's make our code:

p{
  --color:red!important;
  --color:blue; 

  color:var(--color);
  color:blue;
}

Following the same logic, we resolve the cascade for --color and for color. --color:red!important is the winner and the same for color:blue so at the end we have blue because we no more care about color:var(--color).

An important rule is to always consider CSS variables (custom properties) as ordinary properties and not only variables that store values.

Custom properties are ordinary properties, so they can be declared on any element, are resolved with the normal inheritance and cascade rules, can be made conditional with @media and other conditional rules, can be used in HTML’s style attribute, can be read or set using the CSSOM, etc. ref


2) They cannot store URLs

This is a common limitation you will stumble upon one day.

What you cannot do ❌

:root {
  --url:"https://picsum.photos/id/1/200/300";
}
.box {
  background:url(var(--url));
} 

What you should do ✔️

:root {
  --url:url("https://picsum.photos/id/1/200/300");
}
.box {
  background:var(--url);
} 

This limitation is related to how url() is parsed. A bit tricky to explain but as we can see the fix is pretty easy. Always add the url() part within the CSS variable.


3) They can make an invalid value valid!

This one is my favorite quirk and it's the one that will give you a lot of headaches.

Let's start with a basic example:

.box {
  background: red;
  background: linaer-gradient(red, blue);
}

Our .box will have a gradient coloration ... wait, no it has a red background. Ah! I made a typo in linear-*. I can easily notice my mistake because the browser crossed the declaration and used the previous one.

Alt Text

Now, let's introduce a variable:

.box {
  --color:red;
  background: var(--color);
  background: linaer-gradient(var(--color), blue);
}

Test the code and you will see that the background is now transparent and our second declaration is no more crossed because it's now a valid one. You will even notice that the first declaration is the one crossed because the second one overrides it.

What the hell is happening here ??!!

When using a variable within a property the browser will only evaluate the value of such property at "computed-value time" because we need to first know the content of the variable. In such a case, the browser will consider the value as valid when doing the cascade and only later it will become invalid.

In our case, the browser is considering the last declaration after resolving the cascade. Then when doing the evaluation, it seems to be invalid so it will be ignored. We won't get back to the previous declaration since we already resolved the cascade and we end with no background so a transparent one.

You may think such behavior is illogical but it's indeed logical because a value can be valid or invalid based on the CSS variable so the browser cannot really know from the beginning.

.box {
  --color:10px; /* a "valid" variable */
  background: red; /* a "valid" declaration */
  background:linear-gradient(var(--color),blue); /* a "valid" declaration that will override the first one  */
  /* The result is an "invalid" value ... */ 
}


A declaration can be invalid at a computed-value time if it contains a var() that references a custom property with its initial value, as explained above, or if it uses a valid custom property, but the property value, after substituting its var() functions, is invalid. When this happens, the computed value of the property is either the property’s inherited value or its initial value depending on whether the property is inherited or not, respectively, as if the property’s value had been specified as the unset keyword. ref

To use easy words: a CSS variable will make the status of a property in a standby mode until we do the evaluation. Only after the evaluation, we can say if it's valid or invalid. If it's invalid then it's too late, we cannot get back to use another one.

A related Stack Overflow question


4) They can be used unitless

Almost all the tutorials/courses will show you such example:

:root {
 --p: 10px;
}
.box {
  padding: var(--p);
}

But you can also do the following:

:root {
 --p: 10;
}
.box {
  padding: calc(var(--p)*1px);
}

Having the unit in the variable isn't mandatory and in some cases, it's even better to use a unitless value because adding a unit is fairly easy and we may need to use the same value with a different unit.

5) They cannot store the inherit value

Let's consider the following example:

<div class="box">
  <div class="item"></div>
</div>

.box {
  border:2px solid red;
}
.item {
  --b:inherit;
  border:var(--b);
}

Intuitively, we may think that .item will inherit the same border of its parent element because --b contain inherit but it won't (you can try and see).

As I explained in (1), the common mistake is to think that CSS variables will simply store a value that we can use later but no. CSS variables (custom properties) are ordinary properties so inherit apply to them and is not store inside them.

Example:

.box {
  --b:5px solid blue; /* we define the variable on the parent */
}
.item {
  --b:inherit; /* the child will inherit the same value so "5px solid blue"*/
  border:var(--b); /* we will have "5px solid blue" */
}

As you can see, the logic of inheritance applies to them the same way as with common properties.

Worth noting that doing the above is useless because CSS variables are by default inherited. It's like setting inherit to a property that is by default inherited (color for example).

6) They only work from parent to child.

Remember this gold rule: CSS variables always travel from a parent element (or an ancestor) to child elements. They never travel from child to parent or between sibling elements.

This will lead us to the following mistake:

:root {
  --c1: red;
  --c2: blue;
  --grad: linear-gradient(var(--c1),var(--c2);
}
.box {
  --c1: green;
  background:var(--grad);
}

Do you think the background of .box will be linear-gradient(green, blue)? No, it will be linear-gradient(red, blue).

The root element is the uppermost element in the DOM so its an ancestor of our box element and our gold rule says that we can only do parent --> child so --c1 cannot go in the opposite direction to reach the root element, change --grad and then we get back in the other direction to re-send the changed value of --grad.

In such example, the .box will inherit the value of --grad defined with the values of --c1 and --c2 inside root. Changing --c1 will simply change the value of --c1 inside .box, nothing more.

7) They can have strange syntaxes

A last and funny quirk.

Did you know that you can do the following?

body {
  --:red;
  background:var(--);
}

Amazing, right? Yes, a CSS variable can be defined using only the two dashes.

You think the above is crazy, take a look at the following:

body {
 --📕:red;
 --📗:green; 
 --📘:blue;
 --📙:orange;
}

Yes, emojis! you can define your variables using emojis and it works.

The syntax of CSS variables allow almost everything the only requirement is to start with --. You can also start with a number (ex: --1:). Related: Can a css variable name start with a number?

Why not only dashes:

body {
  ---------:red;
  background:var(---------);
}

Or the same variable storing two different values

body {
  --‎​:red;
  --‎:blue;
  background:linear-gradient(90deg, var(--‎​),var(--‎));
}

Try the above and you will get a gradient coloration!

To achieve such magic I am relying on a hidden character that make both of the variables different but visually we see them the same.

on April 15, 2021