CSS
    selectorscascade layershas selectorscopeconditional css

    Modern CSS Selectors, Cascade & Conditional Logic in 2026

    A working map of :has(), cascade layers, @scope and @when/@else, and how they change the way you write CSS in 2026.

    Editor: Paul RadfordAug 21, 202612 min read

    Paul RadfordFull-Stack Developer & Editor. Paul is a full-stack developer and editor of template.tips. Articles here are AI-drafted and reviewed by Paul, who knows the code well enough to catch what's wrong and cut the hype.

    Modern CSS Selectors, Cascade & Conditional Logic in 2026

    What "modern CSS" actually means right now

    CSS in 2026 is not the CSS most working developers learned it as. For a long stretch, the language grew by addition: new properties, new units, new layout modes, while the core mechanics of selecting elements, resolving conflicts, and scoping styles stayed mostly untouched since the 2000s. That has changed. Four features, at four different stages of maturity, now cover the gaps that used to force you into JavaScript, naming conventions, or specificity hacks.

    This page is the map. It explains what each feature does, why it exists, and how the four fit together into one mental model, then routes you to the deep dive for each one:

    • Selecting elements based on their descendants or state, with :has()
    • Controlling which rules win, independent of selector weight, with cascade layers
    • Containing a component's styles so they cannot leak, with @scope
    • Writing conditional style blocks without a JavaScript fallback, with @when/@else

    If you only read this page, you should leave with a correct picture of what problem each feature solves and whether it belongs in your stylesheet today. If you need the exact syntax, browser history, or production patterns, that is what the linked articles are for.

    The problem these four features share

    Look at the four in sequence and a pattern shows up. Each one exists because CSS was missing a specific kind of control:

    • You could not select an element based on what was inside or near it. Only descendants could be targeted from an ancestor, never the reverse.
    • You could not say "this rule should always lose to that rule" without a specificity or source-order fight. Weight and order were the only levers.
    • You could not say "this style only applies inside this exact boundary" without inventing a naming convention and hoping everyone on the team followed it.
    • You could not write a real conditional branch in a stylesheet. Testing multiple conditions meant stacking media and container queries and hoping the combination behaved.

    Selectors, cascade, and now conditional logic. Three different layers of the language, three different long-standing gaps, and in 2026 all three finally have native answers. That is the actual story here, more than any single feature.

    Selecting parents and relatives: the :has() selector

    For years, a parent selector was the most requested, most argued-about missing feature in CSS. The performance concern was reasonable: letting a selector look forward or sideways in the DOM and then decide how to style something backward is a genuinely harder matching problem than the browser was built to solve at speed. That is why it sat unshipped for so long, and why WebKit shipping it in Safari 15.4 mattered so much. It broke a deadlock that had lasted the better part of two decades.

    What :has() actually buys you is the ability to style an element based on its children, its siblings, or its state, using nothing but CSS. A card that needs a different layout because it contains an image. A form field that needs a warning style because a sibling input is invalid. A layout that needs to adapt because a specific combination of children is present. All of these used to require a class toggled by JavaScript, watching the DOM for changes that had nothing to do with user interaction. Now they are selector logic.

    The catch is that :has() is easy to reach for and easy to misuse. Not every case that "could" use :has() should. Some of the same jobs are still better served by a plain class, especially when the condition is something your own JavaScript already knows about and would otherwise have to infer by walking the DOM. The real-world patterns worth shipping, and the ones that look tempting but cause more problems than they solve, are covered in the :has() selector guide. Check current support on caniuse.com before you rely on it for anything load-bearing in older browser targets.

    Controlling who wins: cascade layers

    Selectors decide what a rule can match. Cascade layers decide what happens when two rules match the same element and disagree. That used to be settled entirely by specificity and source order, which is why so many codebases end up with a slow creep toward !important and ever-heavier selectors. Once a team starts fighting specificity with more specificity, there is no way back down. Every new override has to out-rank the last one.

    Cascade layers add a separate axis that sits above specificity entirely. You declare layers, put rules inside them, and the layer order decides priority before specificity is ever consulted. A rule in a later layer beats a rule in an earlier layer even if the earlier rule has a far heavier selector. That means a single low-specificity override rule in the right layer can beat a chain of nested class selectors, without touching !important and without raising anyone's selector weight.

    This matters most at the boundary between your own styles and anything you did not write: a component library, a CSS reset, a third-party widget. Instead of guessing how specific someone else's selectors are and trying to out-weigh them, you put their styles in an earlier layer and yours in a later one, and the order settles it. The mechanics, the ordering rules, and the specific patterns for taming a specificity war that has already gotten out of hand are in the cascade layers guide. The MDN reference for @layer is the right place to check exact ordering behavior when you are not sure.

    Keeping styles where they belong: @scope

    The third gap is containment. A rule like .card img looks harmless the day you write it. Six months later, someone nests an unrelated card-like component inside a card for a completely different reason, and the image inside it quietly picks up styling nobody intended. Nothing broke in an obvious way. There was no error. The selector just kept matching, because descendant selectors have no concept of a boundary. They match all the way down, forever, until someone traces a visual bug back to a rule from a different feature entirely.

    The usual fix has been a naming convention: BEM, or some in-house equivalent, where every selector is scoped by discipline rather than by the language. That works only as long as everyone follows the convention correctly, every time, which is a fragile thing to depend on across a team and across years.

    @scope gives CSS an actual containment boundary instead of a naming promise. You define a scope root and, optionally, a scope limit, and the rules inside only match within that boundary. The nested unrelated component stops being a risk, because the selector simply cannot see past the edge you defined. This is not a replacement for a build system or a framework's component model, it is a native answer to a problem that frameworks and naming conventions were the only defense against before. The details, including how the limit boundary works and where @scope still needs a naming convention alongside it, are in the @scope guide. MDN's @scope reference is worth bookmarking while support is still filling in across browsers.

    Branching logic in a stylesheet: @when/@else

    The fourth feature is the least settled, and it should be treated that way. Right now, testing three separate conditions in CSS means writing three separate conditional blocks, feature queries, media queries, container queries, each independent, and hoping their combination produces the layout you actually intended. There is no real branching. There is no "if this, else if that, else this other thing" in the language, which is exactly the kind of logic every other part of a codebase takes for granted.

    @when and @else are proposed to close that gap: a genuine conditional rule structure in CSS, evaluated in order, with a fallback branch when nothing else matches. It is a meaningful change in what a stylesheet can express, closer to how you'd write a conditional in any programming language than anything CSS has offered before.

    The important caveat, and the reason this section is shorter than the other three: @when/@else is not shipping yet. It belongs in this page because it is the direction selector and cascade logic in CSS is heading, and because planning your architecture with it in mind now will save rework later, not because you should expect to ship it in production today. What the syntax is proposed to look like, which conditions it is meant to combine, and how to structure styles now so the eventual migration is painless, are covered in the @when/@else guide. Track its status through the W3C CSS Working Group rather than assuming any single source has the final word while it is still in progress.

    How the four fit together

    Treat these as four different layers of a single system, not four unrelated features to bolt on individually.

    Cascade layers decide which stylesheet wins in a conflict, at the broadest level: resets, third-party code, your components, your overrides, in whatever order you assign them. That is the outermost layer of decision-making, settled before specificity even enters the picture.

    @scope decides where a set of rules is allowed to apply at all, containing a component's styles to its own boundary so they cannot leak into or be leaked into by anything else nearby.

    :has() decides what a selector can see, letting a rule react to descendants, siblings, and state that used to be invisible to CSS selection entirely.

    @when/@else, once it ships, will decide which branch of a rule set applies under a given condition, replacing chains of independent feature and media queries with something closer to an actual conditional statement.

    Put together, a mature 2026 stylesheet can express: "in this layer, within this scope, if this selector matches this condition, apply this branch." That sentence was not writable in CSS a few years ago. Every clause needed a workaround, usually JavaScript, a naming convention, or a specificity hack. Now most of it is native.

    None of these four replace each other. A team that adopts cascade layers still needs @scope for containment, and a team using @scope confidently still benefits from :has() for state-based selection inside that boundary. They solve different problems that happen to compound: less JavaScript for style logic, fewer specificity accidents, fewer leaks between components, and eventually a real conditional structure to replace the stack of overlapping queries most projects have accumulated.

    A short example of what this replaces

    Consider a card component that needs to look different when it contains an image, and needs its internal image styling to never leak into unrelated nested components, and needs to sit safely below a third-party design system's base styles without a specificity fight.

    Before these features, that was three separate workarounds: a JavaScript class toggle to detect the image, a BEM-style naming convention to contain the styles, and a chain of increasingly specific override selectors to beat the third-party CSS. Three different tools, none of them CSS doing the actual job it was being asked to do.

    With the four features above, that becomes a layer assignment for the override priority, a scope boundary for containment, and a :has() selector for the conditional styling, no JavaScript involved. The syntax and edge cases for each piece are in the respective spoke articles linked throughout this page, since getting any one of the three wrong (an unbounded scope, a layer declared in the wrong order, a :has() selector that matches more than intended) can cause the exact kind of quiet bug this whole set of features was meant to eliminate.

    Who this page is for, and who it is not for

    This is for developers who already write CSS regularly and want an accurate map of what changed in selectors, cascade, and conditional logic, before deciding which of the four to adopt and in what order. It assumes you know what specificity and source order already do, so the explanation of cascade layers can focus on what's new rather than reteaching the basics of the cascade.

    This is not a syntax reference. Each spoke article has the actual rules, browser support notes, and production patterns; this page intentionally stays at the level of "what does this solve and when should I reach for it," because that decision has to come before syntax anyway.

    This is also not the place to start if your target browser support rules out most of these features. :has() has reasonable support following its arrival in Safari and subsequent adoption elsewhere, but cascade layers and @scope are newer, and @when/@else has not shipped anywhere. If you are supporting a wide range of older browsers, check caniuse.com against your actual analytics before committing production code to any of the first three, and treat the fourth as a planning exercise rather than something to write today.

    Finally, this is not an argument that you should adopt all four at once. They solve independent problems. A small project with no third-party CSS to fight has little use for cascade layers. A project with no repeated component patterns has little use for @scope. Read the section above that matches a problem you actually have, then follow that link.

    Where to go from here

    If a parent-selection problem is what brought you here, the practical patterns and pitfalls are in the :has() selector guide. If you are fighting a specificity war with a design system or a legacy stylesheet, start with cascade layers. If a component's styles keep leaking into places they should not reach, @scope is the fix. If you are planning ahead for conditional logic that has not shipped yet but will change how you structure future stylesheets, read @when/@else now rather than after the migration is already overdue.

    Each of the four solves one gap in the language. Understanding how they stack, cascade priority first, then containment, then selection, then eventual conditional branching, is what turns four separate features into one coherent way of writing CSS in 2026.

    Related Articles