{"id":"l-css-1-5","courseId":"css-mastery","lang":"en","kind":"lesson","summary":"Write component styles with native CSS nesting, and use :has() to style an element based on what it contains, including a schedule filter that needs no JavaScript.","html":"<p>Two things bother everyone who maintains Waypoint’s card CSS. The card’s rules are scattered: the base rule here, the hover state twenty lines down, the dark-mode override in another file. And the design team keeps asking for styles that depend on what’s <em>inside</em> the card (“if a session is sold out, dim it”), which used to mean a JavaScript class toggle.</p>\n<p>Native nesting fixes the first, and <code dir=\"ltr\">:has()</code> fixes the second.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">Native nesting</h2>\n<p>You can now nest rules inside rules, in plain CSS, with no build step:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"css\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">session-card.css</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">.session-card</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  padding</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">rem</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  border</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">px</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> solid</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> #e5e7eb</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  border-radius</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">12</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">px</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  h2</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">    margin-block</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 0.5</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">rem</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">    font-size</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1.125</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">rem</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  }</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  &#x26;</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">:hover</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">    border-color</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">#a78bfa</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  }</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  .featured</span><span style=\"color:#116329;--shiki-dark:#7EE787\"> &#x26;</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">    border-width</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">2</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">px</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  }</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  @media</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">width</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> >=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 48</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">rem</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">    padding</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1.5</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">rem</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  }</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p><code dir=\"ltr\">&amp;</code> stands for the parent selector. <code dir=\"ltr\">&amp;:hover</code> means “this card when hovered”, and <code dir=\"ltr\">.featured &amp;</code> means “this card inside <code dir=\"ltr\">.featured</code>”. A nested selector without <code dir=\"ltr\">&amp;</code>, like <code dir=\"ltr\">h2</code>, is treated as a descendant: <code dir=\"ltr\">.session-card h2</code>. Nested <code dir=\"ltr\">@media</code> and <code dir=\"ltr\">@container</code> rules apply to the parent selector, so the padding change lives next to the padding.</p>\n<p>Nested selectors can start with an element name (<code dir=\"ltr\">h2</code>, not <code dir=\"ltr\">&amp; h2</code>). Early implementations required a symbol first; every current browser now supports the relaxed syntax, and nesting has been Baseline since December 2023 (widely available since mid-2026).</p>\n<aside class=\"callout callout-mistake\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\"/><path d=\"M12 9v4\"/><path d=\"M12 17h.01\"/></svg><span><span class=\"callout-kind\">Common mistake</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Writing Sass-style suffixes</span></p>\n<p><code dir=\"ltr\">&amp;__title</code> and <code dir=\"ltr\">&amp;--featured</code> concatenate strings in Sass. Native nesting works on selectors, not text, so it can’t build <code dir=\"ltr\">.session-card__title</code>. If you use BEM, write those classes in full.</p>\n</aside>\n<p>Two more rules keep nesting healthy. Under the hood, <code dir=\"ltr\">&amp;</code> behaves like <code dir=\"ltr\">:is()</code> wrapped around the parent list, so <code dir=\"ltr\">#keynote, .session-card { h2 { … } }</code> gives <em>every</em> nested <code dir=\"ltr\">h2</code> rule ID-level specificity. And stop at two levels deep: a selector you can’t read in one glance is one you can’t override in one glance.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\"><code dir=\"ltr\">:has()</code>: style an element by what it contains</h2>\n<p><code dir=\"ltr\">:has()</code> takes a relative selector and matches the element if that selector finds something:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"css\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">CSS</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">/* A card that contains a speaker photo gets a two-column layout */</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">.session-card:has</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#116329;--shiki-dark:#7EE787\">img</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  display</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">grid</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  grid-template-columns</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">64</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">px</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 1</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">fr</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  gap</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">rem</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">/* Dim sold-out sessions */</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">.session-card:has</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">.badge-full</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  opacity</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0.6</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">/* A label whose following input is invalid after the user typed */</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">label</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">:has</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">+</span><span style=\"color:#116329;--shiki-dark:#7EE787\"> input</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span><span style=\"color:#116329;--shiki-dark:#7EE787\">user-invalid</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  color</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">#b91c1c</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>The last example shows that <code dir=\"ltr\">:has()</code> is more than a “parent selector”: <code dir=\"ltr\">+ input</code> looks at the <em>next sibling</em>, so an earlier element can react to a later one. <code dir=\"ltr\">:user-invalid</code> is the polite cousin of <code dir=\"ltr\">:invalid</code>; it only matches after the user has interacted with the field, so an empty form doesn’t load covered in red.</p>\n<p><code dir=\"ltr\">:has()</code> has two limits worth knowing before you hit them. You can’t nest a <code dir=\"ltr\">:has()</code> inside another <code dir=\"ltr\">:has()</code>, and you can’t put pseudo-elements such as <code dir=\"ltr\">::before</code> inside it. Its specificity follows the <code dir=\"ltr\">:is()</code> rule from earlier in this section: the most specific argument counts, so <code dir=\"ltr\">.session-card:has(#keynote-badge)</code> carries the weight of an ID.</p>\n<h2 id=\"sec-3\" tabindex=\"-1\">A filter bar with no JavaScript</h2>\n<p>Here’s where <code dir=\"ltr\">:has()</code> changes how you build things. Waypoint’s filter bar is a set of checkboxes. Because the checkboxes and the cards share an ancestor, <code dir=\"ltr\">.schedule</code>, the state of a checkbox can drive the cards:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"html\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">HTML</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">section</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> class</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"schedule\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">div</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> class</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"filters\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">label</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>&#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">input</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> type</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"checkbox\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> id</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"hide-full\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">> Hide sold-out&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">label</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  &#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">div</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">article</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> class</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"session-card\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>…&#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">span</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> class</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"badge-full\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>Sold out&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">span</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">article</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">article</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> class</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"session-card\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>…&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">article</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">section</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span></code></pre></div>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"css\" data-index=\"3\"><div class=\"code-head\"><span class=\"code-title\">CSS</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">.schedule:has</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">#hide-full:checked</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">.session-card:has</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">.badge-full</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  display</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">none</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>Read it right to left: hide a card that contains a sold-out badge, when it’s inside a schedule that contains a checked <code dir=\"ltr\">#hide-full</code> box. The browser re-evaluates it the moment the checkbox changes.</p>\n<aside class=\"callout callout-why\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><circle cx=\"12\" cy=\"12\" r=\"6\"/><circle cx=\"12\" cy=\"12\" r=\"2\"/></svg><span><span class=\"callout-kind\">Why this matters</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Why this matters</span></p>\n<p>State that lives in the DOM (checked boxes, open <code dir=\"ltr\">&lt;details&gt;</code>, <code dir=\"ltr\">[aria-expanded]</code>, a focused input) is now something CSS can read directly. Fewer class toggles in JavaScript means fewer places for the UI and the state to disagree.</p>\n</aside>\n<aside class=\"callout callout-tip\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5\"/><path d=\"M9 18h6\"/><path d=\"M10 22h4\"/></svg><span><span class=\"callout-kind\">Tip</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Keep the anchor narrow</span></p>\n<p><code dir=\"ltr\">:has()</code> is fast in modern engines, but <code dir=\"ltr\">body:has(...)</code> or <code dir=\"ltr\">*:has(...)</code> asks the browser to re-check a lot of the page whenever anything inside changes. Anchor on the closest common ancestor, <code dir=\"ltr\">.schedule</code> rather than <code dir=\"ltr\">body</code>, when you can. <code dir=\"ltr\">:has()</code> itself is Baseline widely available.</p>\n</aside>\n<h2 id=\"sec-4\" tabindex=\"-1\">Three more selectors that replace JavaScript</h2>\n<p><code dir=\"ltr\">:has()</code> gets the attention, but a few quieter selectors earn their place on the Waypoint page too:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"css\" data-index=\"4\"><div class=\"code-head\"><span class=\"code-title\">CSS</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">/* Highlight the whole filter bar while any control inside it has focus */</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">.filters:focus-within</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">outline</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">2</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">px</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> solid</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> #a78bfa</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; }</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">/* Every card except sold-out and cancelled ones */</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">.session-card:not</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">.is-full</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">.is-cancelled</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) { </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">cursor</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">pointer</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; }</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">/* Stripe only the visible cards, skipping hidden ones */</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">.session-card:nth-child</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">even</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> of :</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">n</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">ot([hidde</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">n</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">])) { </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">background</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">#f9fafb</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; }</span></span></code></pre></div>\n<p><code dir=\"ltr\">:focus-within</code> matches an element when it or anything inside it has focus. <code dir=\"ltr\">:not()</code> accepts a whole list, so you no longer chain <code dir=\"ltr\">:not(.a):not(.b)</code>. And the <code dir=\"ltr\">of S</code> form of <code dir=\"ltr\">:nth-child()</code> counts only siblings that match <code dir=\"ltr\">S</code>, which fixes zebra stripes that break the moment a filter hides a row. All three work in every current browser.</p>\n<h2 id=\"sec-5\" tabindex=\"-1\">Your turn</h2>\n<p>In the exercise you’ll rewrite the card styles with nesting, then add two <code dir=\"ltr\">:has()</code> rules: one that hides sold-out sessions when the filter box is checked, and one that highlights a card when its “save” checkbox is ticked. That completes the cascade section; next you’ll move from <em>which</em> styles apply to <em>where things go</em>, starting with flexbox.</p>\n","toc":[{"id":"sec-1","text":"Native nesting","level":2},{"id":"sec-2","text":":has(): style an element by what it contains","level":2},{"id":"sec-3","text":"A filter bar with no JavaScript","level":2},{"id":"sec-4","text":"Three more selectors that replace JavaScript","level":2},{"id":"sec-5","text":"Your turn","level":2}],"takeaways":["Native nesting groups a component’s states, children and media queries inside its rule, and <code dir=\"ltr\">&amp;</code> stands for the parent selector.","A nested selector gets the specificity of <code dir=\"ltr\">:is()</code> applied to the parent list, so a parent list containing an ID makes every nested rule ID-heavy.","CSS nesting doesn’t concatenate strings the way Sass does, so <code dir=\"ltr\">&amp;__title</code> will never produce <code dir=\"ltr\">.card__title</code>.","<code dir=\"ltr\">:has()</code> matches an element when the relative selector inside it matches, which lets a parent, or an earlier sibling, react to its descendants or later siblings."],"further":[{"title":"Using CSS nesting (MDN)","url":"https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Nesting/Using"},{"title":":has() (MDN)","url":"https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Selectors/:has"},{"title":"Nesting (web.dev Learn CSS)","url":"https://web.dev/learn/css/nesting"}],"quiz":[{"q":"<p>What selector does this nested rule produce?</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"css\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">CSS</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">.session-card</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  .speaker</span><span style=\"color:#116329;--shiki-dark:#7EE787\"> &#x26;</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">padding</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; }</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n","options":[{"text":"<code dir=\"ltr\">.session-card .speaker</code>","why":"That’s what you’d get without the <code dir=\"ltr\">&amp;</code>, or with <code dir=\"ltr\">&amp; .speaker</code>. Here <code dir=\"ltr\">&amp;</code> comes last, so the card is the descendant."},{"text":"<code dir=\"ltr\">.speaker .session-card</code>","why":"Correct. <code dir=\"ltr\">&amp;</code> marks where the parent selector goes; putting it last means a session card inside a <code dir=\"ltr\">.speaker</code>."},{"text":"<code dir=\"ltr\">.session-card.speaker</code>","why":"That needs <code dir=\"ltr\">&amp;.speaker</code> with no space between them."},{"text":"It’s invalid, because <code dir=\"ltr\">&amp;</code> must come first.","why":"<code dir=\"ltr\">&amp;</code> can appear anywhere in a nested selector, including at the end."}],"answer":1},{"q":"<p>Which selector styles a session card only when it contains an image?</p>","options":[{"text":"<code dir=\"ltr\">.session-card img</code>","why":"That selects the image itself, not the card."},{"text":"<code dir=\"ltr\">.session-card &gt; img:parent</code>","why":"There’s no <code dir=\"ltr\">:parent</code> pseudo-class in CSS; <code dir=\"ltr\">:has()</code> is how you select based on descendants."},{"text":"<code dir=\"ltr\">.session-card:has(img)</code>","why":"Correct. The card matches when the relative selector <code dir=\"ltr\">img</code> finds a descendant."}],"answer":2},{"q":"<p>Which rule hides every non-design card when the <code dir=\"ltr\">#only-design</code> checkbox inside <code dir=\"ltr\">.schedule</code> is checked?</p>","options":[{"text":"<code dir=\"ltr\">.schedule:has(#only-design:checked) .session-card:not(.track-design) { display: none; }</code>","why":"Correct. <code dir=\"ltr\">:has()</code> turns the checkbox state into a condition on the shared ancestor, and the rest of the selector targets the cards."},{"text":"<code dir=\"ltr\">#only-design:checked .session-card:not(.track-design) { display: none; }</code>","why":"That looks for cards inside the checkbox, and inputs can’t have children."},{"text":"<code dir=\"ltr\">#only-design:checked + .session-card:not(.track-design) { display: none; }</code>","why":"The <code dir=\"ltr\">+</code> combinator only reaches the single sibling directly after the checkbox, not cards elsewhere in the schedule."}],"answer":0},{"q":"<p>You write <code dir=\"ltr\">.card { &amp;__title { font-weight: 700; } }</code> out of Sass habit. What happens?</p>","options":[{"text":"It styles elements with the class <code dir=\"ltr\">card__title</code>.","why":"Native nesting doesn’t do string concatenation; <code dir=\"ltr\">&amp;</code> stands for a selector, not text to glue onto."},{"text":"It doesn’t style <code dir=\"ltr\">.card__title</code>; the nested selector doesn’t mean what Sass means.","why":"Correct. To style a BEM element with native nesting you write the full class, for example <code dir=\"ltr\">.card__title</code> at the top level."},{"text":"The browser stops parsing the rest of the stylesheet.","why":"CSS error handling drops what it can’t use and keeps going; it never abandons the stylesheet."}],"answer":1}],"exercise":{"kind":"web","mode":"dom","starter":{"html":"<section class=\"schedule\">\n  <div class=\"filters\">\n    <label><input type=\"checkbox\" id=\"hide-full\"> Hide sold-out sessions</label>\n  </div>\n  <article class=\"session-card\" id=\"c1\">\n    <h2>Shipping View Transitions</h2>\n    <label class=\"save-label\"><input type=\"checkbox\" class=\"save\"> Save</label>\n  </article>\n  <article class=\"session-card\" id=\"c2\">\n    <h2>Grid Layout Workshop</h2>\n    <span class=\"badge-full\">Sold out</span>\n    <label class=\"save-label\"><input type=\"checkbox\" class=\"save\"> Save</label>\n  </article>\n  <article class=\"session-card\" id=\"c3\">\n    <h2>Color Spaces for Humans</h2>\n    <label class=\"save-label\"><input type=\"checkbox\" class=\"save\"> Save</label>\n  </article>\n</section>","css":".session-card {\n  margin-block-end: 12px;\n  padding: 12px 16px;\n  border: 2px solid #e5e7eb;\n  border-radius: 12px;\n}\n.session-card h2 {\n  margin: 0 0 8px;\n  font-size: 1.05rem;\n}\n.session-card:hover {\n  background: #faf5ff;\n}\n.badge-full {\n  display: inline-block;\n  padding: 0.1em 0.6em;\n  border-radius: 999px;\n  background: #fee2e2;\n  color: #991b1b;\n}\n\n/* 1. Rewrite the three .session-card rules above as one nested rule. */\n/* 2. Hide sold-out cards when #hide-full is checked. */\n/* 3. Give a card a #6d28d9 border when its .save box is checked. */\n"},"solution":{"html":"<section class=\"schedule\">\n  <div class=\"filters\">\n    <label><input type=\"checkbox\" id=\"hide-full\"> Hide sold-out sessions</label>\n  </div>\n  <article class=\"session-card\" id=\"c1\">\n    <h2>Shipping View Transitions</h2>\n    <label class=\"save-label\"><input type=\"checkbox\" class=\"save\"> Save</label>\n  </article>\n  <article class=\"session-card\" id=\"c2\">\n    <h2>Grid Layout Workshop</h2>\n    <span class=\"badge-full\">Sold out</span>\n    <label class=\"save-label\"><input type=\"checkbox\" class=\"save\"> Save</label>\n  </article>\n  <article class=\"session-card\" id=\"c3\">\n    <h2>Color Spaces for Humans</h2>\n    <label class=\"save-label\"><input type=\"checkbox\" class=\"save\"> Save</label>\n  </article>\n</section>","css":".session-card {\n  margin-block-end: 12px;\n  padding: 12px 16px;\n  border: 2px solid #e5e7eb;\n  border-radius: 12px;\n\n  h2 {\n    margin: 0 0 8px;\n    font-size: 1.05rem;\n  }\n\n  &:hover {\n    background: #faf5ff;\n  }\n\n  &:has(.save:checked) {\n    border-color: #6d28d9;\n  }\n}\n\n.badge-full {\n  display: inline-block;\n  padding: 0.1em 0.6em;\n  border-radius: 999px;\n  background: #fee2e2;\n  color: #991b1b;\n}\n\n.schedule:has(#hide-full:checked) .session-card:has(.badge-full) {\n  display: none;\n}\n"},"checks":[{"id":"uses-nesting","test":"const all = []; const walk = (rules) => { for (const r of rules) { all.push(r); if (r.cssRules) walk(r.cssRules); } }; for (const s of document.styleSheets) walk(s.cssRules); return all.some((r) => r instanceof CSSStyleRule && r.selectorText === '.session-card' && [...r.cssRules].some((c) => c instanceof CSSStyleRule));"},{"id":"all-visible","test":"return $$('.session-card').every((c) => style(c, 'display') !== 'none');"},{"id":"hides-sold-out","test":"const box = $('#hide-full'); if (!box.checked) await click(box); const ok = style('#c2', 'display') === 'none' && style('#c1', 'display') !== 'none' && style('#c3', 'display') !== 'none'; await click(box); return ok;"},{"id":"shows-again","test":"return !$('#hide-full').checked && style('#c2', 'display') !== 'none';"},{"id":"saved-highlight","test":"const save = $('#c1 .save'); if (!save.checked) await click(save); const ok = style('#c1', 'border-top-color') === 'rgb(109, 40, 217)' && style('#c3', 'border-top-color') === 'rgb(229, 231, 235)'; await click(save); return ok;"}],"title":"Nest the card and filter with :has()","prompt":"<p>Three jobs, all in CSS:</p>\n<ol>\n<li>Rewrite the three <code dir=\"ltr\">.session-card</code> rules (base, <code dir=\"ltr\">h2</code>, <code dir=\"ltr\">:hover</code>) as <strong>one nested rule</strong> on <code dir=\"ltr\">.session-card</code>.</li>\n<li>When the <code dir=\"ltr\">#hide-full</code> checkbox is checked, hide (<code dir=\"ltr\">display: none</code>) every <code dir=\"ltr\">.session-card</code> that contains a <code dir=\"ltr\">.badge-full</code>.</li>\n<li>When a card’s <code dir=\"ltr\">.save</code> checkbox is checked, give that card a <code dir=\"ltr\">#6d28d9</code> border color.</li>\n</ol>\n<p>Don’t add JavaScript or change the HTML.</p>\n","hints":["Inside the <code dir=\"ltr\">.session-card</code> rule, <code dir=\"ltr\">h2 { … }</code> and <code dir=\"ltr\">&amp;:hover { … }</code> replace the separate rules.","For the filter, start from the ancestor that contains both the checkbox and the cards: <code dir=\"ltr\">.schedule:has(#hide-full:checked)</code>.","<code dir=\"ltr\">.schedule:has(#hide-full:checked) .session-card:has(.badge-full) { display: none; }</code> and, nested in the card, <code dir=\"ltr\">&amp;:has(.save:checked) { border-color: #6d28d9; }</code>."],"explanation":"<p>Nesting keeps every state of the card in one block: <code dir=\"ltr\">h2</code> becomes a descendant rule and <code dir=\"ltr\">&amp;:hover</code> attaches the pseudo-class to the card itself. The filter works because <code dir=\"ltr\">.schedule</code> contains both the checkbox and the cards: <code dir=\"ltr\">:has(#hide-full:checked)</code> turns the checkbox state into a condition on that shared ancestor, and <code dir=\"ltr\">.session-card:has(.badge-full)</code> picks out sold-out cards. The save highlight is the same idea one level down: the card matches <code dir=\"ltr\">:has(.save:checked)</code> as soon as its own checkbox is ticked, and the browser re-evaluates both rules on every change, with no JavaScript.</p>\n","checkLabels":{"uses-nesting":"The card styles use native nesting","all-visible":"All cards are visible before filtering","hides-sold-out":"Checking the filter hides only the sold-out card","shows-again":"Unchecking the filter shows it again","saved-highlight":"A saved card gets a violet border"}},"words":699,"readMinutes":3}