What if() actually accepts
Chrome 137 shipped the CSS if() function, and while other browser implementations are likely, their timing is not known. The function conditionally assigns a value to a property based on the value of a CSS variable — a job already handled by media queries and light-dark(), but with a different shape.
The --theme example is the canonical one: set it to "Shamrock" and you get color: hsl(146 50% 3%) with background: hsl(146 50% 40%); any other value falls through to hsl(43 74% 3%) and hsl(43 74% 64%).
:root {
/* Change to fall back to the ‘else’ values */
--theme: "Shamrock";
body {
color: if(style(--theme: "Shamrock"): hsl(146 50% 3%); else: hsl(43 74% 3%));
background: if(style(--theme: "Shamrock"): hsl(146 50% 40%); else: hsl(43 74% 64%));
}
}
Formatting splits the function across lines and reads a little clearer, though the pile of colons and brackets remains:
color: if(
style(--theme: "Shamrock"): hsl(146 50% 3%);
else: hsl(43 74% 3%)
);
What the function will not do, after testing, is condition on anything except a custom property. Ordinary properties, HTML attributes via attr(), and other value sources are all off the table for now.
Theming beyond light and dark
light-dark() covers the two-mode case. if() covers the rest: three or more themes, or light and dark variants of every theme. Adding conditions is just adding clauses:
:root {
/* Shamrock | Saffron | Amethyst */
--theme: "Saffron"; /* ...I choose you! */
body {
color: if(
style(--theme: "Shamrock"): hsl(146 50% 3%);
style(--theme: "Saffron"): hsl(43 74% 3%);
style(--theme: "Amethyst"): hsl(282 47% 3%)
);
background: if(
style(--theme: "Shamrock"): hsl(146 50% 40%);
style(--theme: "Saffron"): hsl(43 74% 64%);
style(--theme: "Amethyst"): hsl(282 47% 56%)
);
transition: 300ms;
}
}
Two behaviors are easy to miss. With no else clause present, a --theme that matches none of Shamrock, Saffron, or Amethyst leaves the element at browser default styles. And when several conditions hold, if() resolves to the value of the first true statement — here, Saffron.
Transitions come along for free. A demo toggling --theme through a UI needs only transition: 300ms next to the if() calls.
Note: when the theme is picked by the user through a control like a radio input,if()isn't required at all —:root:has(#shamrock:checked) { /* Styles */ }handles it. Amit Sheen demonstrates the approach at Smashing Magazine.
For maintainability, the colors move into their own custom properties first, then the if() expressions move into custom properties too:
/* Setup */
:root {
/* Shamrock | Saffron | Amethyst */
--theme: "Shamrock"; /* ...I choose you! */
/* Base colors */
--shamrock: hsl(146 50% 40%);
--saffron: hsl(43 74% 64%);
--amethyst: hsl(282 47% 56%);
/* Base colors, but at 3% lightness */
--shamrock-complementary: hsl(from var(--shamrock) h s 3%);
--saffron-complementary: hsl(from var(--saffron) h s 3%);
--amethyst-complementary: hsl(from var(--amethyst) h s 3%);
--background: if(
style(--theme: "Shamrock"): var(--shamrock);
style(--theme: "Saffron"): var(--saffron);
style(--theme: "Amethyst"): var(--amethyst)
);
--color: if(
style(--theme: "Shamrock"): var(--shamrock-complementary);
style(--theme: "Saffron"): var(--saffron-complementary);
style(--theme: "Amethyst"): var(--amethyst-complementary)
);
/* Usage */
body {
/* One variable, all ifs! */
background: var(--background);
color: var(--color);
accent-color: var(--color);
/* Can’t forget this! */
transition: 300ms;
}
}
Nesting works as well. Dropping light-dark() inside an if() branch inverts the palette for dark mode:
--background: if(
style(--theme: "Shamrock"): light-dark(var(--shamrock), var(--shamrock-complementary));
style(--theme: "Saffron"): light-dark(var(--saffron), var(--saffron-complementary));
style(--theme: "Amethyst"): light-dark(var(--amethyst), var(--amethyst-complementary))
);
if() against container style queries
Container style queries test whether a container carries a given custom property, which puts them close to if(). The same demo rebuilt on style queries:
:root {
/* Shamrock | Saffron | Amethyst */
--theme: "Shamrock"; /* ...I choose you! */
--shamrock: hsl(146 50% 40%);
--saffron: hsl(43 74% 64%);
--amethyst: hsl(282 47% 56%);
--shamrock-complementary: hsl(from var(--shamrock) h s 3%);
--saffron-complementary: hsl(from var(--saffron) h s 3%);
--amethyst-complementary: hsl(from var(--amethyst) h s 3%);
body {
/* Container has chosen Shamrock! */
@container style(--theme: "Shamrock") {
--background: light-dark(var(--shamrock), var(--shamrock-complementary));
--color: light-dark(var(--shamrock-complementary), var(--shamrock));
}
@container style(--theme: "Saffron") {
--background: light-dark(var(--saffron), var(--saffron-complementary));
--color: light-dark(var(--saffron-complementary), var(--saffron));
}
@container style(--theme: "Amethyst") {
--background: light-dark(var(--amethyst), var(--amethyst-complementary));
--color: light-dark(var(--amethyst-complementary), var(--amethyst));
}
background: var(--background);
color: var(--color);
accent-color: var(--color);
transition: 300ms;
}
}
The split is scope: if() facilitates conditional values, container style queries facilitate conditional properties and values. Beyond that, the difference is syntax.
Less-obvious conditions
Three patterns that don't show up in basic theming examples — testing that a custom property exists:
/* Hide icons if variable isn’t set */
.icon {
display: if(
style(--icon-family): inline-block;
else: none
);
}
composing more complex statements:
h1 {
font-size: if(
style(--largerHeadings: true): xxx-large;
style(--theme: "themeWithLargerHeadings"): xxx-large
);
}
and testing whether two custom properties match:
/* If #s2 has the same background as #s1, add a border */
#s2 {
border-top: if(
style(--s2-background: var(--s1-background)): thin solid red
);
}
Arithmetic inside the condition
An if() that tries to consume a calc() result will not evaluate it:
div {
/* 3/3 = 1 */
--calc: calc(3/3);
/* Blue, because if() won’t calculate --calc */
background: if(style(--calc: 1): red; else: blue);
}
The variable must be registered with @property first:
@property --calc {
syntax: "<number>";
initial-value: 0;
inherits: false;
}
Where this leaves us
The syntax is hard to like, and genuinely unreadable when formatted on a single line, but the capability is worth the friction. Two gaps stand out. Ordinary properties in the condition would remove the need to declare a custom property just to branch — for instance color: if(style(background: white): black; style(background: black): white);. And calc() expressions inside if() should evaluate without a prior @property registration.
For now, what exists is enough to build simpler design systems around.



