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:7 min read
A priority badge changing color based on a custom property, driven entirely by an if(style(...)) conditional value in the stylesheet.(opens full-size image in a new tab)
A priority badge changing colour from a single if() value.

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 state can even come straight from the markup. With typed attr(), --priority: attr(data-priority type(<custom-ident>), low); reads <div class="card" data-priority="high"> into the custom property that style() tests, with no inline style or JavaScript at all.

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);
}

if() also works inside the body of a custom function, so a condition that many rules share can live in one @function instead of being repeated in every declaration.

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.

Frequently Asked Questions

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

  • — Added Frequently Asked Questions section
  • — Added cross-reference to tutorial 'CSS Custom Functions with `@function`: Reusable Logic Without Sass'
  • — Added cross-reference to tutorial 'HTML Attributes as Real CSS Values with Typed `attr()`'
  • — Added tag 'style-queries'
  • — Added cross-reference to post 'CSS Container Queries Deep Dive: Named Containers, `cqi` Units, and Style Queries'
  • — Initial publication.
This tutorial is licensed under CC BY 4.0 by the author.

Share this tutorial

Spotted a mistake or something outdated in this tutorial? Suggest a correction.

  • HTML Attributes as Real CSS Values with Typed attr()

    Read HTML attributes as CSS colours, lengths and numbers.
    Typed attr() reads an HTML attribute as a real CSS value — type(<color>), type(<integer>), a unit like % or px, or raw-string — for any property on any element, not just text in content. Covers fallbacks for missing or invalid values, patterns for layout, transitions and view transitions, safe fallbacks in browsers without support, and when a custom property is the better choice.
    Published at:
  • The CSS shape() Function: Responsive clip-path Shapes

    Cut responsive curved shapes with CSS shape() and clip-path.
    Draw responsive clip-path shapes with the CSS shape() function: lines, curves and arcs in %, calc() and custom properties, plus browser support and fallbacks.
    Published at:
  • CSS Custom Functions with @function: Reusable Logic Without Sass

    Native, reusable CSS functions with parameters — no Sass needed.
    Define native CSS custom functions with @function: typed parameters, defaults, fallbacks for older browsers, how they differ from Sass, and when to use them.
    Published at: