Inline conditionals in CSS?

6 min read 0 comments

Last week, the CSS WG resolved to add an inline if() to CSS. But what does that mean, and why is it exciting?

Last week, we had a CSS WG face-to-face meeting in A Coruña, Spain. There is one resolution from that meeting that I’m particularly excited about: the consensus to add an inline if() to CSS. While I was not the first to propose an inline conditional syntax, I did try and scope down the various nonterminating discussions into an MVP that can actually be implemented quickly, discussed ideas with implemenators, and eventually published a concrete proposal and pushed for group resolution. Quite poetically, the relevant discussion occurred on my birthday, so in a way, I got if() as the most unique birthday present ever. 😀

This also comes to show that proposals being rejected is not the end-all for a given feature. It is in fact quite common for features to be rejected for several times before they are accepted: CSS Nesting, :has(), container queries were all simply the last iteration in a series of rejected proposals. if() itself was apparently rejected in 2018 with very similar syntax to what I proposed. What was 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.

I posted about this on a variety of social media, and the response by developers has been overwhelmingly positive:

I even had friends from big companies writing to tell me their internal Slacks blew up about it. This proves what I’ve always suspected, and was part of the case I made to the CSS WG: that this is a huge pain point. Hopefully the amount and intensity of positive reactions will help browsers prioritize this feature and add it to their roadmaps earlier rather than later.

Across all these platforms, besides the “I can’t wait for this to ship!” sentiment being most common, there were a few other recurring questions and a fair bit of confusion that I figured were worth addressing.

FAQ

What is if() for? Does it replace style queries?

Quite the opposite — if() complements style queries. If you can do something with style queries, by all means, use style queries — they are almost certainly a better solution. But there are things you simply cannot do with style queries. Let me explain.

The motivating use case was that components (in the broader sense) often need to define higher level custom properties, whose values are not just used verbatim in declarations, but that set unrelated values on a variety of declarations.

For example, consider a --variant custom property (inspired from Shoelace’s variant attribute). It could look like this:

--variant: success | danger | warning | primary | none;

This needs to set background colors, border colors, text colors, icons, etc. In fact, it’s actual value is not used verbatim anywhere, it is only used to set other values.

Style queries get us halfway there:

.callout { /* or :host if in Shadow DOM */
	@container (style(--variant: success)) {
		&::before {
			content: var(--icon-success);
			color: var(--color-success);
		}
	}

	/* (other variants) */
}

However, style queries only work on descendants. We cannot do this:

.callout {
	@container (style(--variant: success)) {
		border-color: var(--color-success-30);
		background-color: var(--color-success-95);

		&::before {
			content: var(--icon-success);
			color: var(--color-success-05);
		}
	}

	/* (other variants) */
}

Often the declarations we need to set on the element itself are very few, sometimes even just one. However, even one is one too many and makes using custom properties untenable for many (possibly most) higher level custom property use cases. As a result, component libraries end up resorting to presentational attributes like pill, outline, size, etc.

While presentational attributes may seem fine at first glance, or even better for DX (fewer characters — at least compared to setting a variable per element), they have several usability issues:

Reduced flexibility
They cannot be conditionally applied based on selectors, media queries, etc. Changing them requires more JS. If they are used within another component, you’re SOL, whereas with (inheritable) custom properties, you can set the property on the parent component and it will inherit down.
Verbosity
They have to be applied to individual instances, and cannot be inherited. Even if one uses some form of templating or componentization to reduce duplication, they still have to wade through these attributes when debugging with dev tools.
Lack of consistency
Since almost every mature component also supports custom properties, users have to remember which styling is done via attributes and which via custom properties. The distinction is often arbitrary, as it’s not driven by use cases, but implementation convenience.

With if(), the above example becomes possible, albeit with worse ergonomics than style queries since it cannot cascade (though I do have a proposal to allow it to — plus all other IACVT declarations):

.callout {
	border-color: if(
		style(--variant: success) ? var(--color-success-30) :
		style(--variant: danger) ? var(--color-danger-30) :
		/* (other variants) */
		var(--color-neutral-30)
	);
	background-color: if(
		style(--variant: success) ? var(--color-success-95) :
		style(--variant: danger) ? var(--color-danger-95) :
		/* (other variants) */
		var(--color-neutral-95)
	);

	@container (style(--variant: success)) {
		&::before {
			content: var(--icon-success);
			color: var(--color-success-05);
		}
	}

	/* (other variants) */
}

While this was the primary use case, it turned out that it’s pretty easy to also make media queries and supports conditions part of if()’s conditional syntax. And since it’s a function, its arguments (including the condition!) can be stored in other custom properties. This means you can do things like this:

:root {
	--xl: media(width > 1600px);
	--l: media (width > 1200px);
	--m: media (width > 800px);
}

and then define values like:

padding: if(
	var(--xl) ? var(--size-3) :
	var(--l) or var(--m) ? var(--size-2) :
	var(--size-1)
);

Just like ternaries in JS, it may also be more ergonomic for cases where only a small part of the value varies:

animation: if(media(prefers-reduced-motion) ? 10s : 1s) rainbow infinite;

So is it in browsers yet?

Believe it or not, that was a real question I got 😅. No, it’s not in browsers yet, and it won’t be for a while. The most optimistic estimate is 2 years or so, if the process doesn’t stall at any point (as it often does).

All we have is consensus to work on the feature. The next steps are:

  1. Reach consensus on the syntax of the feature. Syntax debates can often take a very long time, because syntax is an area where everyone has opinions. The current debates revolve around:
  2. Spec the feature.
  3. Get the first implementation. Often that is the hardest part. Once one browser implements, it is far easier to get the others on board.
  4. Get it shipped across all major browsers.

I do have a page where I track some of my standards proposals which should help illuminate what the timeline looks like for each of these steps. In fact, you can track the progress of if() specifically there too.

Is this the first conditional in CSS?

Many responses were along the lines of “Wow, CSS is finally getting conditionals!”.

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

In addition:

Does this make CSS imperative?

A widespread misconception is that non-linear logic (conditionals, loops) makes a language imperative.

Declarative vs imperative is not about logic, but level of abstraction. Are we describing the goal or how to achieve it? In culinary terms, a recipe is imperative, a restaurant menu is declarative

Conditional logic can actually make a language more declarative if it helps describe intent better.

Consider the following two snippets of CSS:

Space toggleif()
button {
	border-radius: calc(.2em + var(--pill, 999em));
}

.fancy.button {
	/* Turn pill on */
	--pill: initial;
}
button {
	border-radius: if(style(--shape: pill) ? 999em : .2em);
}

.fancy.button {
	--shape: pill;
}

I would argue the latter is far more declarative, i.e. much closer to specifying the goal rather than how to achieve it.

Does this make CSS a programming language?

A very common type of response was around whether CSS is now a programming language (either asking whether it is, or asserting that it now is). To answer that, one first needs to answer what a programming language is.

If it’s Turing-completeness that makes a language a programming language, then CSS has been a programming language for over a decade. But then again, so is Excel or Minecraft. So what does that even mean?

If it’s imperativeness, then no, CSS is not a programming language. But neither are many actual programming languages!

But a deeper question is, why does it matter? Is it because it legitimizes choosing to specialize in CSS? It is because you can then be considered a programmer even if you only write HTML & CSS? If this only matters for optics, then we should fix the issue at its core and fight to legitimize CSS expertise regardless of whether CSS is a programming language. After all, as anyone who knows several well-respected programming languages and CSS can attest, CSS is far harder to master.


Great as all this may be, it won’t be in browsers for a while. What can we do right now? I wrote Part 2 exactly about that: CSS Conditionals, now?