Four lines of CSS, one card, and every paragraph loses its margin. I went through this example in Gabriel Shoyombo's new Smashing Magazine piece twice before it clicked, and once it did I started looking sideways at a few :has() rules of my own.
The article is long and covers four ways a move from JavaScript to modern CSS can make a codebase harder to change. The cascade one is the part you can check in your own stylesheet today. Nothing here is new to the platform. It's how :has() and native nesting already behave, and how easy that behaviour is to miss in a diff.
A card with no hero image
The goal is simple: remove paragraph margins from cards that have a hero image. Here's the markup from the article:
<article class="card"> <p>Body text of the card.</p> <p>A second paragraph.</p> <div class="comment"> <img src="avatar.png" alt=""> <p>Nice post.</p> </div> </article> And the CSS that looks like it does the job:
.card:has(img) p { margin: 0; } /* Elsewhere, the comment component's own rule */ .comment p { margin-block: 8px; } This card has no hero image. Every paragraph in it loses its margin anyway, the comment's included.
Two separate mistakes in one selector
The first is about matching. :has(img) matches any descendant image, so the comment's avatar counts as a hero image. The p after it matches any descendant paragraph, so the comment's text gets pulled in too.
The second is about weight, and it's the one that would have caught me. Shoyombo cites MDN's specificity guide: :has() adds no specificity by itself, but its argument does. That puts .card:has(img) p at 0-1-2. The comment's own .comment p sits at 0-1-1, so the card rule wins a fight the comment component didn't know it was in.
It also depends on runtime state. The article's scenario is a user posting a comment that includes an avatar, and the body text of the card above it losing its spacing. That's not a bug you'll reproduce from a ticket that says spacing looks off sometimes.
Nesting pulls the same trick
Native nesting borrows weight in a similar way. Per the MDN nesting guide the article quotes, the & selector takes "the largest specificity in the associated selector list," exactly like :is(). Here's the article's adaptation of the MDN example:
/* Specificity 1-0-0 */ .card, #featured { /* Specificity: 1-0-1 */ & a { color: blue; } } /* Specificity: 0-1-0 */ .button { color: red; } A .button link inside a .card comes out blue, even though there's no #featured anywhere near it. The ID in the list lends its weight to every branch.
Three ways to put the weight back
Constrain the match first. Child combinators stop the selector at the card's own children:
.card:has(> img) > p { margin: 0; } Then drop the borrowed weight with :where(), which always has a specificity of 0-0-0. The selector list stays and the ID's weight goes:
/* :where() always weighs 0-0-0 */ :where(.card, #featured) { /* Specificity: 0-0-1 */ & a { color: blue; } } With that change, plain links in a card are still blue and the .button link is red again.
Finally, check your layers, because they don't fix this on their own. The article quotes MDN's cascade introduction: "normal layered styles have less precedence than normal unlayered styles." In Shoyombo's test, a layered rule with a specificity of 0-4-0 lost to an unlayered .title at 0-1-0. So if new component styles go into @layer components and the legacy stylesheet stays unlayered, the legacy rule wins every conflict. The exception is !important, which reverses that order. His point is that layers only help when both sides of a conflict are layered.
Browser support and the performance numbers
The article doesn't discuss browser support for :has(), nesting, :where() or @layer, so I'm not going to fill that in. Everything above is about selector matching and the cascade, not about which browsers ship what.
The performance section does name a browser. Shoyombo ran a 5,000-row list in Chrome 153 on a Mac in October 2026 and says he tested only Chrome. Two :has() rules that each cost almost nothing alone restyled 5,001 elements per toggle when they shared a stylesheet, at about 7ms each time. At 20,000 rows that rose to about 31ms. He asks readers to take the pattern rather than the numbers: one page, one browser, one machine.
What I'm watching next
Shoyombo says he has filed the restyle behaviour with the Chromium team as an issue, with a reduced test case. I'd like to see that result, and the same test run in other engines. Until then, my own rule is short. Anything with :has() or & in it gets a > where it can take one, and a :where() wherever an ID or a heavy argument would otherwise leak its weight into the rest of the selector.