CSS
    selectorsfrontend

    CSS @when/@else: Writing Conditional Styles Without JavaScript

    Explore the new CSS @when and @else at-rules that bring conditional logic directly into stylesheets. This article covers practical use cases for creating state-dependent styles, responsive behavior without media queries, and theme switching that actually makes sense.

    Editor: Paul RadfordJul 2, 20267 min read
    CSS @when/@else: Writing Conditional Styles Without JavaScript

    CSS @when/@else: Writing Conditional Styles Without JavaScript

    The CSS @when and @else at-rules let you write actual if/else branching logic in a stylesheet, testing media features, support for properties, and container conditions in one unified conditional block. They are defined in the CSS Conditional Rules Level 5 draft and aim to replace the patchwork of separate @media, @supports, and @container blocks developers currently stitch together, often with JavaScript filling the gaps between them.

    That gap-filling is the part worth paying attention to. Right now, if you want to apply a style when a browser lacks grid support, and a different style when a container is wide enough, and yet another when the user prefers dark mode, you either write three separate conditional blocks and hope the cascade sorts itself out, or you reach for JavaScript to detect conditions and toggle classes. @when/@else collapses that into a single readable chain. It is not shipping in any browser as a stable feature yet, but it is worth understanding now because it changes how you should structure conditional CSS going forward.

    What problem does @when actually solve?

    CSS already has three separate conditional at-rules: @media for viewport and device features, @supports for feature detection, and @container for container queries. Each works fine on its own, but none of them talk to each other, and none of them have a real "else" branch. You end up writing the positive case and then writing the negative case as a second, disconnected rule block that you have to keep in sync manually.

    css
    1/* Today: two disconnected rules, easy to let them drift out of sync */
    2@supports (display: grid) {
    3 .layout { display: grid; grid-template-columns: repeat(3, 1fr); }
    4}
    5@supports not (display: grid) {
    6 .layout { display: flex; flex-wrap: wrap; }
    7}

    That works, but it does not scale past two branches without becoming unreadable, and there is no way to mix a @supports test and a @media test in the same conditional chain without nesting one inside the other. @when fixes both problems: it gives you @else if and @else, and it lets you combine media(), supports(), and container() feature tests with and, or, and not inside a single condition.

    How does the @when/@else syntax work?

    The draft spec wraps each existing conditional type in a function you call inside @when. Here is the same grid/flex fallback rewritten with a third branch for older browsers that support neither:

    css
    1/* Proposed syntax per CSS Conditional Rules Level 5 */
    2.layout {
    3 display: block; /* baseline, always applied first */
    4}
    5
    6@when supports(display: grid) and media(width >;= 768px) {
    7 .layout {
    8 display: grid;
    9 grid-template-columns: repeat(3, 1fr);
    10 gap: 1.5rem;
    11 }
    12} @else if supports(display: flex) {
    13 .layout {
    14 display: flex;
    15 flex-wrap: wrap;
    16 gap: 1.5rem;
    17 }
    18} @else {
    19 /* Neither grid nor flex, browser falls back to block display,
    20 which is already set above and simply is not overridden */
    21}

    Notice the structure reads top to bottom like a real conditional statement in a programming language, because that is exactly what it is. You are no longer relying on cascade specificity and rule ordering to fake an else branch; the browser evaluates the chain and applies exactly one matching block. That is a meaningful shift for anyone who has debugged a stylesheet where a "fallback" rule accidentally won the cascade war against the "modern" rule because of source order.

    How do you handle theme switching without JavaScript?

    Theme switching is the use case that sells this feature fastest, because the JavaScript version is genuinely annoying to get right. You typically read prefers-color-scheme, check for a stored user override in localStorage, and toggle a class on <html> before first paint to avoid a flash of the wrong theme. With @when, you can express "user preference wins, otherwise fall back to system preference, otherwise default to light" directly in CSS:

    css
    1/* Base (light) theme variables, always applied as the fallback */
    2: root {
    3 --bg: #ffffff;
    4 --text: #111111;
    5}
    6
    7@when media(prefers-color-scheme: dark) {
    8 : root {
    9 --bg: #0f0f10;
    10 --text: #f2f2f2;
    11 }
    12} @else if media(prefers-color-scheme: no-preference) {
    13 : root {
    14 --bg: #ffffff;
    15 --text: #111111;
    16 }
    17}
    18
    19/* A manual override still wins because it's a separate, more specific rule */
    20[data-theme="dark"] {
    21 --bg: #0f0f10;
    22 --text: #f2f2f2;
    23}

    This does not fully replace a manual toggle system (you still need an attribute or class for the "user explicitly picked dark mode" case, since @when cannot read localStorage), but it does remove the JavaScript that previously handled the "respect system preference by default" branch. You get a flicker-free default theme with zero script execution on first paint, which is a real, measurable win for Largest Contentful Paint on theme-heavy sites.

    Can @when replace JavaScript-driven responsive components?

    Partially, and this is where you need to be honest about limits. Container queries already let you style a component based on its own box size rather than the viewport, which killed off a lot of ResizeObserver boilerplate. @when adds the ability to combine that container check with a support check in one place, which matters for components you ship to unknown embedding contexts (design systems, widgets, CMS blocks):

    css
    1.card-grid {
    2 container-type: inline-size;
    3}
    4
    5@when container(min-width: 480px) and supports(gap: 1rem) {
    6 .card-grid {
    7 display: grid;
    8 grid-template-columns: repeat(2, 1fr);
    9 gap: 1rem;
    10 }
    11} @else if container(min-width: 480px) {
    12 .card-grid {
    13 display: flex;
    14 flex-wrap: wrap;
    15 }
    16 .card-grid > * {
    17 margin: 0.5rem; /* manual gap fallback for old flex implementations */
    18 }
    19} @else {
    20 .card-grid {
    21 display: block;
    22 }
    23}

    What this does not replace is any behavior that depends on runtime state CSS cannot see: scroll position beyond scroll-driven animations, network conditions, or data fetched after load. If your "responsive" component logic is actually business logic (show a different card layout for premium users, say), that decision still belongs in JavaScript or your templating layer. @when replaces the CSS you were writing to react to environment and support conditions, not the CSS you were writing to react to application state.

    Is @when ready to use in production?

    No, and you should not pretend otherwise. As of now, @when and @else exist only in the CSS Conditional Rules Module Level 5 draft; no stable release of Chrome, Firefox, or Safari implements it, and the syntax in the draft is still subject to change before any browser ships it. Treat every example in this article as "syntax to learn," not "syntax to ship."

    The good news is that adopting it early costs you almost nothing, because of how CSS handles unknown rules. Per the CSS Syntax Module error handling rules, a browser that does not recognize an at-rule simply discards it and moves on, rather than throwing an error or breaking the rest of the stylesheet. That means you can write your real, working fallback using @media, @supports (see MDN's @supports reference), and @container (see MDN's @container reference) first, then layer a @when block after it as pure progressive enhancement:

    css
    1/* Real fallback, works everywhere today */
    2@supports (display: grid) {
    3 .layout { display: grid; }
    4}
    5
    6/* Speculative enhancement, ignored by every browser right now,
    7 picked up automatically once support lands, no code change needed */
    8@when supports(display: grid) and media(width >;= 768px) {
    9 .layout { display: grid; grid-template-columns: repeat(3, 1fr); }
    10}

    One gotcha worth flagging: this safe-ignore behavior applies to the unknown at-rule itself, not to invalid values inside a rule your browser does understand. Don't assume you can mix speculative and stable syntax inside the same declaration block and expect graceful degradation, test each rule block in isolation. Also keep an eye on :has() support if you plan to combine it with future @when conditions; :has() shipped broadly starting with Safari 15.4, Chrome 105, and Firefox 121, per MDN's :has() documentation, but older Firefox versions still in the wild will silently ignore selectors using it, so pair it with an explicit @supports selector(:has(*)) check rather than assuming.

    Key Takeaways

    • @when/@else are defined in the CSS Conditional Rules Level 5 draft and unify @media, @supports, and @container tests into one branching syntax with real else-if logic, something none of those at-rules support individually today.
    • No browser ships stable support as of now; write your production fallback with existing @supports/@media/@container and layer @when underneath it, since unrecognized at-rules are discarded per the CSS Syntax specification rather than breaking the page.
    • Theme switching is the strongest current use case: an @when media(prefers-color-scheme: dark) chain removes the pre-paint JavaScript that toggles theme classes for system-preference detection, cutting flash-of-wrong-theme bugs.
    • @when replaces CSS you write to react to environment and support conditions (viewport size, container size, feature support), not JavaScript that encodes actual application or business state.
    • :has() support (Safari 15.4+, Chrome 105+, Firefox 121+) is a good preview of the adoption curve to expect for @when: expect a multi-year gap between spec draft and safe production use without fallbacks.

    Advertisement

    728 × 90 — Leaderboard — Google AdSense

    Related Articles