“If” CSS Gets Inline Conditionals

4 months ago 60

DigitalOcean provides cloud products for every stage of your journey. Get started with $200 in free credit!

A few sirens went off a couple of weeks ago when the CSS Working Group (CSSWG) resolved to add an if() conditional to the CSS Values Module Level 5 specification. It was Lea Verou’s X post that same day that caught my attention:

A historical day for CSS 😀🎉

If you write any components used and/or styled by others, you know how huge this is!

background: if(style(–variant: success), var(–green));

Even if you don’t, this will allow things like:
padding: if(var(–2xl), 1em, var(–xl) or var(–m),… pic.twitter.com/cXeqwBuXvK

— Lea Verou (@LeaVerou) June 13, 2024

Lea is the one who opened the GitHub issue leading to the discussion and in a stroke of coincidence — or serendipity, perhaps — the resolution came in on her birthday. That had to be quite a whirlwind of a day! What did you get for your birthday? “Oh, you know, just an accepted proposal to the CSS spec.” Wild, just wild.

The accepted proposal is a green light for the CSSWG to work on the idea with the intent of circulating a draft specification for further input and considerations en route to, hopefully, become a recommended CSS feature. So, it’s gonna be a hot minute before any of this is baked, that is, if it gets fully baked.

But the idea of applying styles based on a conditional requirement is super exciting and worth an early look at the idea. I scribbled some notes about it on my blog the same day Lea posted to X and thought I’d distill those here for posterity while rounding up more details that have come up since then.

This isn’t a new idea

Many proposals are born from previously rejected proposals and if() is no different. And, indeed, we have gained several CSS features in recent days that allow for conditional styling — :has() and Container Style Queries being two of the more obvious examples. Lea even cites a 2018 ticket that looks and reads a lot like the accepted proposal.

The difference?

Style queries had already shipped, and we could simply reference the same syntax for conditions (plus media() and supports() from Tab’s @when proposal) whereas in the 2018 proposal how conditions would work was largely undefined.

Lea Verou, “Inline conditionals in CSS?”

I like how Lea points out that CSS goes on to describe how CSS has always been a conditional language:

Folks… CSS had conditionals from the very beginning. Every selector is essentially a conditional!

Lea Verou, “Inline conditionals in CSS?”

True! The Cascade is the vehicle for evaluating selectors and matching them to HTML elements on a page. What if() brings to the table is a way to write inline conditions with selectors.

Syntax

It boils down to this:

<if()> = if( <container-query>, [<declaration-value>]{1, 2} )

…where:

  • Values can be nested to produce multiple branches.
  • If a third argument is not provided, it becomes equivalent to an empty token stream.

All of this is conceptual at the moment and nothing is set in stone. We’re likely to see things change as the CSSWG works on the feature. But as it currently stands, the idea seems to revolve around specifying a condition, and setting one of two declared styles — one as the “default” style, and one as the “updated” style when a match occurs.

.element { background-color: /* If the style declares the following custom property: */ if(style(--variant: success), var(--color-green-50), /* Matched condition */ var(--color-blue-50); /* Default style */ ); }

In this case, we’re looking for a style() condition where a CSS variable called --variant is declared and is set to a value of success, and:

  • …if --variant is set to success, we set the value of success to --color-green-50 which is a variable mapped to some greenish color value.
  • …if --variant is not set to success, we set the value of the success to --color-blue-50 which is a variable mapped to some bluish color value.

The default style would be optional, so I think it can be omitted in some cases for slightly better legibility:

.element { background-color: /* If the style declares the following custom property: */ if(style(--variant: success), var(--color-green-50) /* Matched condition */ ); }

The syntax definition up top mentions that we could support a third argument in addition to the matched condition and default style that allows us to nest conditions within conditions:

background-color: if( style(--variant: success), var(--color-success-60), if(style(--variant: warning), var(--color-warning-60), if(style(--variant: danger), var(--color-danger-60), if(style(--variant: primary), var(--color-primary) ) ), ) );

Oomph, looks like some wild inception is happening in there! Lea goes on to suggest a syntax that would result in a much flatter structure:

<if()> = if( [ <container-query>, [<declaration-value>]{2} ]#{0, }, <container-query>, [<declaration-value>]{1, 2} )

In other words, nested conditions are much more flat as they can be declared outside of the initial condition. Same concept as before, but a different syntax:

background-color: if( style(--variant: success), var(--color-success-60), style(--variant: warning), var(--color-warning-60), style(--variant: danger), var(--color-danger-60), style(--variant: primary), var(--color-primary) );

So, rather than one if() statement inside another if() statement, we can lump all of the possible matching conditions into a single statement.

We’re attempting to match an if() condition by querying an element’s styles. There is no corresponding size() function for querying dimensions — container queries implicitly assume size:

.element { background: var(--color-primary); /* Condition */ @container parent (width >= 60ch) { /* Applied styles */ background: var(--color-success-60); } }

And container queries become style queries when we call the style() function instead:

.element { background: orangered; /* Condition */ @container parent style(--variant: success) { /* Applied styles */ background: dodgerblue; } }

Style queries make a lot more sense to me when they’re viewed in the context of if(). Without if(), it’s easy to question the general usefulness of style queries. But in this light, it’s clear that style queries are part of a much bigger picture that goes beyond container queries alone.

There’s still plenty of things to suss out with the if() syntax. For example, Tab Atkins describes a possible scenario that could lead to confusion between what is the matched condition and default style parameters. So, who knows how this all shakes out in the end!

Conditions supporting other conditions

As we’ve already noted, if() is far from the only type of conditional check already provided in CSS. What would it look like to write an inline conditional statement that checks for other conditions, such as @supports and @media?

In code:

background-color: if( supports( /* etc. */ ), @media( /* etc. */ ) );

The challenge would be container supporting size queries. As mentioned earlier, there is no explicit size() function; instead it’s more like an anonymous function.

@andruud has a succinctly describes the challenge in the GitHub discussion:

I don’t see why we couldn’t do supports() and media(), but size queries would cause cycles with layout that are hard/impossible to even detect. (That’s why we needed the restrictions we currently have for size CQs in the first place.

“Can’t we already do this with [X] approach?”

When we were looking at the syntax earlier, you may have noticed that if() is just as much about custom properties as it is about conditionals. Several workarounds have emerged over the years to mimic what we’d gain if() we could set a custom property value conditionally, including:

  • Using custom properties as a Boolean to apply styles or not depending on whether it is equal to 0 or 1. (Ana has a wonderful article on this.)
  • Using a placeholder custom property with an empty value that’s set when another custom property is set, i.e. “the custom property toggle trick” as Chris describes it.
  • Container Style Queries! The problem (besides lack of implementation) is that containers only apply styles to their descendants, i.e., they cannot apply styles to themselves when they meet a certain condition, only its contents.

Lea gets deep into this in a separate post titled “Inline conditional statements in CSS, now?” that includes a table that outlines and compares approaches, which I’ll simply paste below. The explanations are full of complex CSS nerdery but are extremely helpful for understanding the need for if() and how it compares to the clever “hacks” we’ve used for years.

MethodInput valuesOutput valuesProsConsBinary Linear InterpolationTogglesPaused animationsType GrindingVariable animation name
NumbersQuantitativeCan be used as part of a valueLimited output range
var(--alias) (actual values are too weird to expose raw)AnyCan be used in part of a valueWeird values that need to be aliased
NumbersAnyNormal, decoupled declarationsTakes over animation property

Cascade weirdness

KeywordsAny value supported by the syntax descriptorHigh flexibility for exposed APIGood encapsulationMust insert CSS into light DOM

Tedious code (though can be automated with build tools)

No Firefox support (though that’s changing)

KeywordsAnyNormal, decoupled declarationsImpractical outside of Shadow DOM due to name clashes

Takes over animation property

Cascade weirdness

Happy birthday, Lea!

Belated by two weeks, but thanks for sharing the spoils of your big day with us! 🎂

References

Direct Link →

Read Entire Article