Conditional CSS Values with the if() Function
Introduction
Conditional styling in CSS has always meant one of two workarounds: toggling a class from JavaScript, or duplicating
a whole rule inside @media or @supports. The new if() function collapses that into a single property
value. It picks between several values, right inside a declaration, based on a condition CSS can already evaluate:
a media query, a feature check, or — the genuinely new part — the current value of a custom property.
Basic Usage
.badge {
background: if(
style(--priority: high): crimson;
style(--priority: medium): darkorange;
else: seagreen
);
}How to read it:
- Each branch is a
condition: valuepair, and branches are separated by semicolons. - Branches are checked top to bottom, and the first match wins. Later branches are never considered once one matches.
else:is the catch-all, used when no condition matches.
Change --priority — from JavaScript, or from another CSS rule — and the background follows. There's no class
list to keep in sync with the state.
The Three Condition Types
.card {
width: if(
media(width > 900px): 32rem;
supports(color: oklch(0.7 0.15 30)): 24rem;
else: 100%
);
}media()takes the same conditions as@media, such as viewport width orprefers-color-scheme.supports()takes the same conditions as@supports, checking whether the browser understands a property and value.style()checks a custom property's current value. This is what makesif()new: a value, not a whole block, that reacts to state.
Where style() Reads From
style() tests the custom property's value on the element being styled — whether that value was set on the
element itself or inherited from an ancestor. That's why the demo below can set --priority once on the card and
have the badge inside it react. (Container style queries work differently: they read the value from the
container.)
Try It Live
Pick a priority. JavaScript only sets the --priority custom property on the card; the stylesheet's if() chain
decides the badge's color. Choose None to remove the property and watch the else branch take over.
If your browser doesn't support if() yet, every option shows the same gray, and the readout under the controls
tells you so. That gray comes from a plain background declared just before the if() version — see
Always Pair It with a Fallback.
Conditional CSS Values with if()
A priority badge whose color is chosen entirely by if(style(--priority: ...)) in the stylesheet — pick a priority, or None to see the else branch, while a readout shows the live --priority value and whether your browser supports if() or is using the plain-gray fallback.
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>CSS if() Playground</title> </head> <body> <main class="playground"> <header class="playground-header"> <h1>Priority Badge, styled with CSS if()</h1> <p>Pick a priority. JavaScript only sets <code>--priority</code>; the badge's color is chosen entirely in the stylesheet by an <code>if(style(...))</code> chain.</p> </header> <section class="demo-area" aria-label="Badge preview"> <div class="card" id="card"> <span class="badge" id="badge">Priority: none</span> <p class="card-body">One declaration in the stylesheet covers every priority, with no class per state.</p> </div> </section> <fieldset class="control-group"> <legend>--priority</legend> <div class="segmented-control" role="radiogroup" aria-label="Priority"> <label class="segment"><input type="radio" name="priority" value="none" checked><span>None</span></label> <label class="segment"><input type="radio" name="priority" value="low"><span>Low</span></label> <label class="segment"><input type="radio" name="priority" value="medium"><span>Medium</span></label> <label class="segment"><input type="radio" name="priority" value="high"><span>High</span></label> </div> <p class="tip">"None" removes <code>--priority</code>, so no <code>style()</code> test matches and the <code>else</code> branch picks the gray.</p> </fieldset> <output class="readout" id="readout" aria-live="polite"></output> </main> <script src="./index.js"></script> </body> </html>
Starting sandbox…
No console output yet.
Branch Order Matters
Because the first matching branch wins, put the most specific conditions first. A broad condition placed early hides every narrower one after it:
/* ❌ The 900px branch never runs: any width over 900px is also over 600px */
.panel {
padding: if(
media(width > 600px): 1.5rem;
media(width > 900px): 2.5rem;
else: 1rem
);
}
/* ✅ Narrowest condition first */
.panel {
padding: if(
media(width > 900px): 2.5rem;
media(width > 600px): 1.5rem;
else: 1rem
);
}Always Pair It with a Fallback
Browser support is still limited, so declare a plain value first and the if() version second:
.badge {
background: gray; /* Every browser reads this */
background: if(
style(--priority: high): crimson;
else: gray
);
}This protects two different cases in two different ways:
- Browsers without
if()can't parse the second declaration, so they drop it while reading the stylesheet and only the first one ever reaches the cascade. No@supportsblock is needed. - Browsers with
if()use the second declaration, and the first one is simply overridden. If no branch matches and there's noelse, the property becomes invalid at computed-value time and behaves as if it were never set. The earlier fallback does not come back — so always include anelse.
Going Further
Combining Conditions
Conditions can be combined with and, or, and not, just like in @media:
.toolbar {
gap: if(
media(width > 900px) and style(--density: compact): 0.5rem;
not media(width > 900px): 0.25rem;
else: 1rem
);
}Using if() Inside Other Values
if() stands in for part of a value, so it can sit inside a shorthand, a calc(), or a custom property:
.card {
/* Only the inline margin changes */
margin: 0 if(media(width > 900px): 2rem; else: 1rem);
/* One branch of a larger calculation */
padding: calc(if(style(--density: compact): 0.5rem; else: 1rem) * 2);
/* Resolve once, reuse everywhere */
--accent: if(style(--priority: high): crimson; else: royalblue);
border-color: var(--accent);
color: var(--accent);
}Relationship to @media, @supports and @container
/* Before: the same rule duplicated per breakpoint */
.panel { padding: 1rem; }
@media (width > 900px) {
.panel { padding: 2rem; }
}
/* After: one declaration */
.panel {
padding: if(media(width > 900px): 2rem; else: 1rem);
}The at-rules still own conditional rules: new selectors, several declarations at once, or restructuring a layout.
if() is for a conditional value inside a rule you're already writing — exactly the case that used to force
either duplicating the whole rule or falling back to JavaScript. If the value should scale smoothly rather than jump
between fixed options, min(), max(), and clamp() are usually the better
fit.
Where Should You Use This?
- A single custom property (
--variant,--size,--state) driving several unrelated declarations across a component, without a class for every combination - Small value swaps (a color, a spacing step, a font-weight) that don't justify duplicating an entire rule under
@media - Replacing a JavaScript-driven inline style or class toggle whose only job was picking between a few known CSS values
Production Considerations & Edge Cases
- Check support before relying on it.
if()shipped in Chrome and Edge 137. Check current Firefox and Safari support on Can I use before depending on it without a fallback. - Declare the fallback first, and always include
else. The fallback covers browsers withoutif();elsecovers browsers with it. You need both (see Always Pair It with a Fallback). - Feature-detect with
@supportswhen a whole block depends on it.@supports (color: if(else: red))is true only in browsers that understandif(). style()only reads custom properties in current browsers. A test likestyle(color: red)on a standard property isn't supported yet; branch on a--custom-propertyinstead.- It's a value, not a control-flow statement.
if()can't leave out a declaration or a selector. For that,@media,@supports, and@containerare still what you reach for.
Key Takeaway
if() fills the one gap the conditional at-rules never covered: a single value, inside a declaration you're
already writing, chosen by a condition — most usefully the live value of a custom property. Order branches from
most to least specific, always end with else, and keep a plain fallback declared first while browser support
catches up.
Changelog
- — Initial publication.