CSS scoped custom property ignored when used to calculate variable in outer scope

In your case you have evaluated the --scale custom property at the root level to define the --size-* properties and then you defined the --scale again inside child elements. This will not trigger evaluation again because it was already done in an upper level.

Here is a simple example to illustrate the issue:

.box {
  --color: var(--c, blue);
}

span {
  color: var(--color);
}
<div>
  <div class="box"><!-- --c is evaluated at this level -->
    <span style="--c:red">I will not be red because the property is already evaluated and --color is set to blue using the default value</span>
  </div>
</div>

<div style="--c:red">
  <div class="box"><!-- --c is evaluated at this level -->
    <span>I will be red because at the time of the evaluation --c is red (inherited from the upper div)</span>
  </div>
</div>

To fix your issue, you need to move the declaration from :root to the same level as the --scale definition:

.scale {
  --size-1: calc(1 * var(--scale, 1) * 1rem);
  --size-2: calc(2 * var(--scale, 1) * 1rem);
  --size-3: calc(3 * var(--scale, 1) * 1rem);
}

.size-1 { font-size: var(--size-1) }
.size-2 { font-size: var(--size-2) }
.size-3 { font-size: var(--size-3) }

.scale-1x { --scale: 1 }
.scale-2x { --scale: 2 }
.scale-3x { --scale: 3 }


html {
  font: 1em sans-serif;
  background: papayawhip;
}

ol {
  float: left;
  list-style: none;
  margin: 1rem;
}
<ol class="scale-1x scale">
  <li class="size-1">size 1</li>
  <li class="size-2">size 2</li>
  <li class="size-3">size 3</li>
</ol>
<ol class="scale-2x scale">
  <li class="size-1">size 1</li>
  <li class="size-2">size 2</li>
  <li class="size-3">size 3</li>
</ol>
<ol class="scale-3x scale">
  <li class="size-1">size 1</li>
  <li class="size-2">size 2</li>
  <li class="size-3">size 3</li>
</ol>

In this case, --scale is defined at the same level as its evaluation so --size-* will be defined correctly for each case.


From the specification:

To substitute a var() in a property’s value:

  1. If the custom property named by the first argument to the var()
    function is animation-tainted, and the var() function is being used in
    the animation property or one of its longhands, treat the custom
    property as having its initial value for the rest of this algorithm.
  2. If the value of the custom property named by the first argument to the
    var() function is anything but the initial value, replace the var()
    function by the value of the corresponding custom property. Otherwise,
  3. if the var() function has a fallback value as its second argument,
    replace the var() function by the fallback value. If there are any
    var() references in the fallback, substitute them as well.
  4. Otherwise, the property containing the var() function is invalid at
    computed-value time

In your first situation, you are falling into 3 because there is no value specified for --scale at the root level. In the last case we are falling into 2 because we defined --scale at the same level and we have its value.


In all the cases, we should avoid any evaluation at :root level because it’s simply useless. The root level is the uppermost level in the DOM so all the elements will inherit the same value and it’s impossible to have different values inside the DOM unless we evaluate the variable again.

Your code is equivalent to this one:

:root {
  --size-1: calc(1 * 1 * 1rem);
  --size-2: calc(2 * 1 * 1rem);
  --size-3: calc(3 * 1 * 1rem);
}

Let’s take another example:

:root {
  --r:0;
  --g:0;
  --b:255;
  --color:rgb(var(--r),var(--g),var(--b))
}
div {
  color:var(--color);
}
p {
  --g:100;
  color:var(--color);
}
<div>
  some text
</div>
<p>
  some text
</p>

Intuitively, we may think that we can change the --color by changing one of the 3 variables defined at :root level but we cannot do this and the above code is the same as this one:

:root {
  --color:rgb(0,0,255)
}
div {
  color:var(--color);
}
p {
  --g:100;
  color:var(--color);
}
<div>
  some text
</div>
<p>
  some text
</p>

The 3 variables (--r, --g, --b) are evaluated inside :root so we have already substituted them with their values.

In such a situation we have 3 possibilities:

  • Change the variables inside the :root using JS or another CSS rule. This won’t allow us to have different colors:
:root {
  --r:0;
  --g:0;
  --b:255;
  --color:rgb(var(--r),var(--g),var(--b))
}
div {
  color:var(--color);
}
p {
  --g:200; /*this will not have any effect !*/
  color:var(--color);
}

:root {
  --g:200; /*this will work*/
}
<div>
  some text
</div>
<p>
  some text
</p>
  • Evaluate the variable again inside the needed element. In this case, we will lose any kind of flexibility and the definition inside :root will become useless (or at least will become the default value):
:root {
  --r:0;
  --g:0;
  --b:255;
  --color:rgb(var(--r),var(--g),var(--b))
}
div {
  color:var(--color);
}
p {
  --g:200;
  --color:rgb(var(--r),var(--g),var(--b));
  color:var(--color);
}
<div>
  some text
</div>
<p>
  some text
</p>
  • Change the :root selector with the universal selector *. This will make sure our function is defined and evaluated at all the levels. In some complex case, this may have some unwanted results
* {
  --r:0;
  --g:0;
  --b:255;
  --color:rgb(var(--r),var(--g),var(--b))
}
div {
  color:var(--color);
}
p {
  --g:200;
  color:var(--color);
}
<div>
  some text
</div>
<p>
  some text
</p>

Considering this, we should always keep the evaluation at the lowest possible point in the DOM tree and especially after the variable changes (or at the same level)

Here is what we shouldn’t do

:root {
  --r: 0;
  --g: 0;
  --b: 0;
}
.color {
  --color: rgb(var(--r), var(--g), var(--b))
}
.green {
  --g: 255;
}
.red {
  --r: 255;
}
p {
  color: var(--color);
}

h1 {
  border-bottom: 1px solid var(--color);
}
<div class="color">
  <h1 class="red">Red </h1>
  <p class="red">I want to be red :(</p>
</div>
<div class="color">
  <h1 class="green">Green </h1>
  <p class="green">I want to be green :(</p>
</div>

Here is what we should do

:root {
  --r:0;
  --g:0;
  --b:0;
}
.color {
  --color:rgb(var(--r),var(--g),var(--b));
}

.green {
  --g:255;
}

.red {
  --r:255;
}

p {
  color:var(--color);
}
h1 {
  border-bottom: 1px solid var(--color);
}
<div class="red">
  <h1 class="color">Red title</h1>
  <p class="color">Yes I am red :D</p>
</div>
<div class="green">
  <h1 class="color">Green title</h1>
  <p class="color">Yes I am green :D</p>
</div>

We can also do like this:

:root {
  --r:0;
  --g:0;
  --b:0;
}
.color {
  --color:rgb(var(--r),var(--g),var(--b));
}

.green {
  --g:255;
}

.red {
  --r:255;
}

p {
  color:var(--color);
}
h1 {
  border-bottom: 1px solid var(--color);
}
<div class="red color">
  <h1 >Red title</h1>
  <p >Yes I am red :D</p>
</div>
<div class="green color">
  <h1>Green title</h1>
  <p >Yes I am green :D</p>
</div>

Leave a Comment