Skip to main content

Harshal V. LADHE

Conditional CSS Values with the if() Function

Swap CSS values by media, supports, or style conditions.
Published at:
Last updated:
Estimated reading time:5 min read

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: value pair, 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 or prefers-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 makes if() 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>

Read-only
Ln –, Col –HTML1.7 KBUTF-8

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 @supports block is needed.
  • Browsers with if() use the second declaration, and the first one is simply overridden. If no branch matches and there's no else, 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 an else.

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 without if(); else covers browsers with it. You need both (see Always Pair It with a Fallback).
  • Feature-detect with @supports when a whole block depends on it. @supports (color: if(else: red)) is true only in browsers that understand if().
  • style() only reads custom properties in current browsers. A test like style(color: red) on a standard property isn't supported yet; branch on a --custom-property instead.
  • It's a value, not a control-flow statement. if() can't leave out a declaration or a selector. For that, @media, @supports, and @container are 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.

Categories:CSS
Tags:

Changelog

  • — Initial publication.
This tutorial is licensed under CC BY 4.0 by the author.

Share this tutorial