Conditions for CSS Variables

I'll start from this: there are no (There is a mod­ule named “CSS Con­di­tional Rules”, but don't ex­pect it to cover the CSS vari­ables —it cov­ers some at-rules stuff. There is even a pro­posal for @when/@else at-rules, which, again, do not any­thing in com­mon with vari­ables.) con­di­tions in specs to use with CSS vari­ables. I think that this is a re­ally big flaw in specs, as while vari­ables al­ready pro­vide a lot of things that were not pos­si­ble in any other way be­fore, the ab­sence of con­di­tions is re­ally frus­trat­ing, as there could be a lot of uses for them.

But what if we'd need those imag­i­nary con­di­tional state­ments for our CSS vari­ables now? Well, as with a lot of other CSS stuff, we can hack our way around for same cases.

The Problem's Definition

So, what we need is a way to use a sin­gle CSS vari­able for set­ting dif­fer­ent CSS prop­er­ties to dif­fer­ent val­ues, but not based di­rectly on this vari­able (that is —those val­ues shouldn't be cal­cu­lated from our vari­able). We need con­di­tions.

Using Calculations for Binary Conditions

Long story short, I'll just pre­sent the so­lu­tion to you right now and would ex­plain it later:

:root {
    --is-big: 0;
}

.is-big {
    --is-big: 1;
}

.block {
    padding: calc(
        25px * var(--is-big) +
        10px * (1 - var(--is-big))
    );
    border-width: calc(
        3px * var(--is-big) +
        1px * (1 - var(--is-big))
    );
}

In this ex­am­ple, we're mak­ing all our el­e­ments with .block to have paddings equal to 10px and bor­der widths to 1px un­less the --is-big vari­able on those el­e­ments won't be 1, in which case they would be­come 25px and 3px re­spec­tively.

The mech­a­nism be­yond this is rather sim­ple: we use both our pos­si­ble val­ues in a sin­gle cal­cu­la­tion us­ing calc(), where we nul­lify one and keep an­other value based on the vari­able's value which can be ei­ther 1 or 0. In other words, we'll have 25px * 1 + 10px * 0 in one case and 25px * 0 + 10px * 1 in an­other.

More Complex Conditions

We can use this method to choose not only from 2 pos­si­ble val­ues but for choos­ing from 3 or more val­ues. How­ever, for each new added pos­si­ble value the cal­cu­la­tion be­comes more com­plex. For choos­ing be­tween 3 pos­si­ble val­ues it would al­ready look like this:

.block {
    padding: calc(
        100px * (1 - var(--foo)) * (2 - var(--foo)) * 0.5 +
         20px * var(--foo) * (2 - var(--foo)) +
          3px * var(--foo) * (1 - var(--foo)) * -0.5
    );
}

This could ac­cept 0, 1 and 2 val­ues for --foo vari­able and cal­cu­late the padding to 100px, 20px or 3px cor­re­spond­ingly.

The prin­ci­ple is the same: we just need to mul­ti­ply each pos­si­ble value to an ex­pres­sion that would be equal to 1 when the con­di­tion for this value is the one we need and to 0 in other cases. And this ex­pres­sion can be com­posed rather eas­ily: we just need to nul­lify each other pos­si­ble value of our con­di­tional vari­able. Af­ter do­ing this we'd need to add our trig­ger­ing value there to see if we'd need to ad­just the re­sult so it would be equal to 1. And that's it.

A Possible Trap in the Specs

With the in­creas­ing com­plex­ity of such cal­cu­la­tions, there is a chance at one point they would stop from work­ing. Why? There is this note in specs:

UAs must sup­port calc() ex­pres­sions of at least 20 terms, where each NUM­BER, DI­MEN­SION, or PER­CENT­AGE is a term. If a calc() ex­pres­sion con­tains more than the sup­ported num­ber of terms, it must be treated as if it were in­valid.

Of course, I tested this a bit and couldn't found such lim­i­ta­tions in the browsers I tested, but there is still a chance ei­ther you would write some re­ally com­plex code that would meet the pos­si­ble ex­ist­ing limit, or some of the browsers could in­tro­duce this limit in the fu­ture, so be care­ful when us­ing re­ally com­plex cal­cu­la­tions.

Conditions for Colors

As you can see, those cal­cu­la­tions could be used only for things that you can cal­cu­late, so there is no chance we could use it for switch­ing the val­ues of dis­play prop­erty or any other non-nu­meric ones. But what about col­ors? Ac­tu­ally, we can cal­cu­late the in­di­vid­ual com­po­nents of the col­ors. Sadly, right now it would work only in We­bkits and Blinks, as Fire­fox don't yet sup­port calc() in­side rgba() and other color func­tions.

But when the sup­port would be there (or if you'd like to ex­per­i­ment on this in browsers with an ex­ist­ing sup­port), we could do things like that:

:root {
    --is-red: 0;
}

.block {
    background: rgba(
        calc(
            255*var(--is-red) +
            0*(1 - var(--is-red))
            ),
        calc(
            0*var(--is-red) +
            255*(1 - var(--is-red))
            ),
        0, 1);
}

Here we'd have lime color by de­fault and red if the --is-red would be set to 1 (note that when the com­po­nent could be zero we could just omit it at all, mak­ing out code more com­pact, here I kept those for clar­ity of an al­go­rithm).

As you could do those cal­cu­la­tions with any com­po­nents, it is pos­si­ble to cre­ate those con­di­tions for any col­ors (and maybe even for gra­di­ents? You should try it!).

Another Trap in the Specs

When I was test­ing how the con­di­tions work for col­ors, I found out a re­ally, re­ally weird lim­i­ta­tion in Specs (Tab Atkins com­mented that this is­sue with color com­po­nents was fixed in the specs (but is not yet sup­ported by browsers). Yay! Also he said that as an­other so­lu­tion we could just use per­cent­ages in­side rgba, I to­tally for­got about this fea­ture, haha.). It is called Type Check­ing. I now of­fi­cially hate it. What this means is that if the prop­erty ac­cepts only <in­te­ger> as a value, if you'd have any di­vi­sions or non-in­te­gers in­side the calc() for it, even if the re­sult would be in­te­ger, the “re­solved type” wouldn't be <in­te­ger>, it would be <num­ber>, and that means that those prop­er­ties won't ac­cept such val­ues. And when we'd have cal­cu­la­tions in­volv­ing more than two pos­si­ble val­ues, we'd need to have a non-in­te­ger mod­i­fiers. And that would make our cal­cu­la­tion in­valid for us­ing with col­ors or other in­te­ger-only prop­er­ties (like z-in­dex).

That is:

calc(255 * (1 - var(--bar)) * (var(--bar) - 2) * -0.5)

Would be in­valid when in­side of the rgba(). Ini­tially I thought that this be­hav­iour is a bug, es­pe­cially know­ing how the color func­tions can ac­tu­ally ac­cept the val­ues that go be­yond the pos­si­ble ranges (you can do rgba(9001, +9001, -9001, 42) and get a valid yel­low color), but this typ­ing thing seems to be too hard for browsers to handle.

Solutions?

There is one far from per­fect so­lu­tion. As in our case we know both the de­sired value and the prob­lem­atic mod­i­fier, we can pre-cal­cu­late them and then round it up. Yep, that means that the re­sult­ing value could be not ex­actly the same, as we would lose some pre­ci­sion in some cases. But it is bet­ter than noth­ing, right?

But there is an­other so­lu­tion that would work for col­ors —we can use hsla in­stead of rgba, as it ac­cepts not in­te­gers, but num­bers and per­cent­ages, so there won't be a con­flict in type re­solv­ing. But for other prop­er­ties like z-in­dex that so­lu­tion won't work. But even with this method there still could be some losses in pre­ci­sion if you're go­ing to con­vert rgb to hsl. But those should be less than in pre­vi­ous so­lution.

Preprocessing

When the con­di­tions are bi­nary it is still pos­si­ble to write them by hand. But when we're start­ing to use more com­plex con­di­tions, or when we're get­ting to the col­ors, we'd bet­ter have tools that could make it eas­ier to write. Luck­ily, we have pre­proces­sors for this purpose.

Here is how I man­aged to quickly do it in Sty­lus (You can look at Code­Pen with this code in ac­tion.):

conditional($var, $values...)
  $result = ''

  // If there is only an array passed, use its contents
  if length($values) == 1
    $values = $values[0]

  // Validating the values and check if we need to do anything at all
  $type = null
  $equal = true

  for $value, $i in $values
    if $i > 0 and $value != $values[0]
      $equal = false

    $value_type = typeof($value)
    $type = $type || $value_type
    if !($type == 'unit' or $type == 'rgba')
      error('Conditional function can accept only numbers or colors')

    if $type != $value_type
      error('Conditional function can accept only same type values')

  // If all the values are equal, just return one of them
  if $equal
    return $values[0]

  // Handling numbers
  if $type == 'unit'
    $result = 'calc('
    $i_count = 0
    for $value, $i in $values
      $multiplier = ''
      $modifier = 1
      $j_count = 0
      for $j in 0..(length($values) - 1)
        if $j != $i
          $j_count = $j_count + 1
          // We could use just the general multiplier,
          // but for 0 and 1 we can simplify it a bit.
          if $j == 0
            $modifier = $modifier * $i
            $multiplier = $multiplier + $var
          else if $j == 1
            $modifier = $modifier * ($j - $i)
            $multiplier = $multiplier + '(1 - ' + $var + ')'
          else
            $modifier = $modifier * ($i - $j)
            $multiplier = $multiplier + '(' + $var + ' - ' + $j + ')'

          if $j_count < length($values) - 1
            $multiplier = $multiplier + ' * '

      // If value is zero, just don't add it there lol
      if $value != 0
        if $modifier != 1
          $multiplier = $multiplier + ' * ' + (1 / $modifier)
        $result = $result + ($i_count > 0 ? ' + ' : '') + $value + ' * ' + $multiplier
        $i_count = $i_count + 1

    $result = $result + ')'

  // Handling colors
  if $type == 'rgba'
    $hues = ()
    $saturations = ()
    $lightnesses = ()
    $alphas = ()

    for $value in $values
      push($hues, unit(hue($value), ''))
      push($saturations, saturation($value))
      push($lightnesses, lightness($value))
      push($alphas, alpha($value))

    $result = 'hsla(' + conditional($var, $hues) + ', ' + conditional($var, $saturations) + ', ' + conditional($var, $lightnesses) + ', ' + conditional($var, $alphas) +  ')'

  return unquote($result)

Yep, there is a lot of code, but this mixin can gen­er­ate con­di­tion­als both for num­bers and col­ors, and not only for two pos­si­ble con­di­tions but for many more.

The us­age is re­ally easy:

border-width: conditional(var(--foo), 10px, 20px)

The first ar­gu­ment is our vari­able, the sec­ond one is the value that should be ap­plied when the vari­able would be equal to 0, the third —when it would be equal to 1, etc.

This above call would gen­er­ate proper con­di­tional:

border-width: calc(10px * (1 - var(--foo)) + 20px * var(--foo));

And here is a more com­plex ex­am­ple for the color con­di­tionals:

color: conditional(var(--bar), red, lime, rebeccapurple, orange)

Would gen­er­ate some­thing that you surely wouldn't want to write by hand:

color: hsla(calc(120 * var(--bar) * (var(--bar) - 2) * (var(--bar) - 3) * 0.5 + 270 * var(--bar) * (1 - var(--bar)) * (var(--bar) - 3) * 0.5 + 38.82352941176471 * var(--bar) * (1 - var(--bar)) * (var(--bar) - 2) * -0.16666666666666666), calc(100% * (1 - var(--bar)) * (var(--bar) - 2) * (var(--bar) - 3) * 0.16666666666666666 + 100% * var(--bar) * (var(--bar) - 2) * (var(--bar) - 3) * 0.5 + 49.99999999999999% * var(--bar) * (1 - var(--bar)) * (var(--bar) - 3) * 0.5 + 100% * var(--bar) * (1 - var(--bar)) * (var(--bar) - 2) * -0.16666666666666666), calc(50% * (1 - var(--bar)) * (var(--bar) - 2) * (var(--bar) - 3) * 0.16666666666666666 + 50% * var(--bar) * (var(--bar) - 2) * (var(--bar) - 3) * 0.5 + 40% * var(--bar) * (1 - var(--bar)) * (var(--bar) - 3) * 0.5 + 50% * var(--bar) * (1 - var(--bar)) * (var(--bar) - 2) * -0.16666666666666666), 1);

Note that there is no de­tec­tion of <in­te­ger>-ac­cept­ing prop­er­ties, so that won't work for z-in­dex and such, but it al­ready con­verts col­ors to hsla() to make them man­age­ble (though even this could be en­hanced so this con­ver­ta­tion would hap­pen only when it would be needed). An­other thing I didn't im­ple­ment in this mixin (yet?) is the abil­ity to use CSS vari­ables for the val­ues. This would be pos­si­ble for non-in­te­ger num­bers as those val­ues would be in­serted as is in the con­di­tional cal­cu­la­tions. Maybe, when I'll find time, I'll fix the mixin to ac­cept not only num­bers or col­ors but also vari­ables. For the time be­ing it is still pos­si­ble to do us­ing the al­go­rithm ex­plained in this ar­ticle.

Fallbacks

Of course, if you're plan­ning to ac­tu­ally use this, you'll need to have a way to set fall­backs. They're easy for browsers that just don't sup­port vari­ables: you just de­clare the fall­back value be­fore the con­di­tional de­c­la­ration:

.block {
    padding: 100px; /* fallback */
    padding: calc(
        100px * ((1 - var(--foo)) * (2 - var(--foo)) / 2) +
         20px * (var(--foo) * (2 - var(--foo))) +
          3px * (var(--foo) * (1 - var(--foo)) / -2)
    );
}

But when it comes to col­ors we have a prob­lem: when there is a sup­port for vari­ables, in fact (and that's an­other re­ally weird place in specs), just any de­c­la­ra­tion con­tain­ing vari­ables would be con­sid­ered valid. And this means that it is not pos­si­ble in CSS to make a fall­back for some­thing con­tain­ing vari­ables:

background: blue;
background: I 💩 CSS VAR(--I)ABLES;

Is valid CSS and per specs, the back­ground would get an ini­tial value, not the one pro­vided in a fall­back (even though it is ob­vi­ous that the other parts of the value are in­cor­rect).

So, what we need in or­der to pro­vide a fall­back in those cases —add @sup­port wrap­per that would test the sup­port for every­thing ex­cept for the vari­ables.

In our case, we need to wrap our con­di­tional col­ors for Fire­fox in some­thing like this:

.block {
    color: #f00;
}
@supports (color: rgb(0, calc(0), 0)) {
    .block {
        color: rgba(calc(255 * (1 - var(--foo))), calc(255 * var(--foo)), 0, 1);
  }
}

Here we're test­ing a sup­port for cal­cu­la­tions in­side color func­tions and ap­ply­ing the con­di­tional color only in that case.

It is also pos­si­ble to cre­ate such fall­backs au­to­mat­i­cally, but I won't rec­om­mend you to use pre­proces­sors for them as the com­plex­ity of cre­at­ing such stuff is much more than the ca­pa­bil­i­ties pre­proces­sors provide.

Use Cases

I re­ally don't like to pro­vide use cases for the things the need for which is ob­vi­ous. So I'll be brief. And I'll state not only the con­di­tions for vari­ables, but also the gen­eral con­di­tions, like for the re­sult of calc().

  • The con­di­tions for CSS vari­ables would be per­fect for themi­fy­ing blocks. This way you could have a num­ber of num­bered themes and then ap­ply them to blocks (and nested ones!) us­ing just one CSS vari­able like --block-vari­ant: 1. This is not some­thing that is pos­si­ble through any other means other than vari­ables and when you'd want to have dif­fer­ent val­ues for dif­fer­ent props in dif­fer­ent themes, with­out the con­di­tion­als you'd need to have many dif­fer­ent vari­ables and ap­ply all of them in every case.

  • Ty­pog­ra­phy. If it was pos­si­ble to use the <, <=, > and >= in con­di­tions for vari­ables, it would be pos­si­ble to have a num­ber of “rules” for dif­fer­ent font sizes, so you could set dif­fer­ent line heights, font weights and other prop­er­ties based on the given font-size. This is pos­si­ble now, but now when you need to have some “stops” for those val­uea and not just the val­ues de­rived from ems.

  • Re­spon­sive de­sign. Well, if there were the con­di­tions for cal­cu­la­tions, then it would be al­most the same as those elu­sive “el­e­ment queries” —you could check the vw or the par­ent's widths in per­cents and de­cide what to ap­ply in dif­fer­ent cases.

There can be other use cases, tell me if you'd find one! I'm sure I had more of them my­self, but I don't have that good of a mem­ory to re­mem­ber all the things I ever wanted to do with CSS. Be­cause its all the things.

Future

I would re­ally like to see con­di­tions de­scribed in CSS specs, so we would not rely on calc hacks and could use proper con­di­tions for non-cal­cu­lat­able val­ues too. It is also im­pos­si­ble right now to have con­di­tions other than strict equal­ity, so no “when the vari­able is more than X” and other stuff like that. I don't see any rea­sons why we can't have proper con­di­tions in CSS, so if you know a fel­low spec de­vel­oper, hint them about this is­sue. My only hope is that they won't tell us to “just use JS” or find out ex­cuses of why that wouldn't ever be pos­si­ble. Here, it is al­ready pos­si­ble now us­ing the hacks, there can't be any ex­cuses.