Con­di­tions for CSS Vari­ables

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 Prob­lem's De­f­i­n­ition

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 Cal­cu­la­tions for Bi­nary Con­ditions

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))
    );
    bor­der-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 Com­plex Con­ditions

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 Pos­si­ble 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.

Con­di­tions 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 {
    back­ground: 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.

Solu­tions?

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.

Pre­pro­cessing

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.):

con­di­tional($var, $val­ues...)
  $re­sult = ''

  // If there is only an ar­ray passed, use its con­tents
  if length($val­ues) == 1
    $val­ues = $val­ues[0]

  // Val­i­dat­ing the val­ues and check if we need to do any­thing at all
  $type = null
  $equal = true

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

    $val­ue_­type = typeof($value)
    $type = $type || $val­ue_­type
    if !($type == 'unit' or $type == 'rgba')
      er­ror('Con­di­tional func­tion can ac­cept only num­bers or col­ors')

    if $type != $val­ue_­type
      er­ror('Con­di­tional func­tion can ac­cept only same type val­ues')

  // If all the val­ues are equal, just re­turn one of them
  if $equal
    re­turn $val­ues[0]

  // Han­dling num­bers
  if $type == 'unit'
    $re­sult = 'calc('
    $i_­count = 0
    for $value, $i in $val­ues
      $mul­ti­plier = ''
      $mod­i­fier = 1
      $j_­count = 0
      for $j in 0..(length($val­ues) - 1)
        if $j != $i
          $j_­count = $j_­count + 1
          // We could use just the gen­eral mul­ti­plier,
          // but for 0 and 1 we can sim­plify it a bit.
          if $j == 0
            $mod­i­fier = $mod­i­fier * $i
            $mul­ti­plier = $mul­ti­plier + $var
          else if $j == 1
            $mod­i­fier = $mod­i­fier * ($j - $i)
            $mul­ti­plier = $mul­ti­plier + '(1 - ' + $var + ')'
          else
            $mod­i­fier = $mod­i­fier * ($i - $j)
            $mul­ti­plier = $mul­ti­plier + '(' + $var + ' - ' + $j + ')'

          if $j_­count < length($val­ues) - 1
            $mul­ti­plier = $mul­ti­plier + ' * '

      // If value is zero, just don't add it there lol
      if $value != 0
        if $mod­i­fier != 1
          $mul­ti­plier = $mul­ti­plier + ' * ' + (1 / $mod­i­fier)
        $re­sult = $re­sult + ($i_­count > 0 ? ' + ' : '') + $value + ' * ' + $mul­ti­plier
        $i_­count = $i_­count + 1

    $re­sult = $re­sult + ')'

  // Han­dling col­ors
  if $type == 'rgba'
    $hues = ()
    $sat­u­ra­tions = ()
    $light­nesses = ()
    $al­phas = ()

    for $value in $val­ues
      push($hues, unit(hue($value), ''))
      push($sat­u­ra­tions, sat­u­ra­tion($value))
      push($light­nesses, light­ness($value))
      push($al­phas, al­pha($value))

    $re­sult = 'hsla(' + con­di­tional($var, $hues) + ', ' + con­di­tional($var, $sat­u­ra­tions) + ', ' + con­di­tional($var, $light­nesses) + ', ' + con­di­tional($var, $al­phas) +  ')'

  re­turn un­quote($re­sult)

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:

bor­der-width: con­di­tional(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:

bor­der-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: con­di­tional(var(--bar), red, lime, re­bec­ca­pur­ple, or­ange)

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; /* fall­back */
    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:

back­ground: blue;
back­ground: 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;
}
@sup­ports (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.