Skip to main content
Observatory

★ Zone 7

:has()

CSS finally learned to look up.

Fill in all fields to see the magic Getting there — keep going… ✦ All fields complete — pure CSS detected this
01
02
03

Active :has() selectors right now

  • fieldset:has(input:focus)
  • fieldset:has(input:not(:placeholder-shown))
  • form:has(input:not(:placeholder-shown):not(:first-of-type))
● Chrome 105+ ● Firefox 121+ ● Safari 15.4+ Baseline 2023

The parent selector CSS never had

For decades, CSS could only select elements based on their ancestors, not their children. You could style a parent that contained a certain kind of child — but only in your head. In the stylesheet, you were powerless.

Developers hacked around this with JavaScript: listen for an event, walk the DOM, add a class to the parent, style that class. Three steps to do something that should be one.

:has() ended that. It lets any selector look inside itself — at its children, its descendants, even what follows it in the DOM. The colon-has pattern unlocks parent selection, sibling-based logic, and state-driven composition that used to require a full framework.

The form above proves it. Every label recolor, every fieldset highlight, every button state change, and the final completion celebration — all triggered by :has(). Not one event listener.

Core patterns

Pattern 1

Parent selection

Style a container based on what it contains.

/* Style a card that contains an img */ .card:has(img) { display: grid; grid-template-rows: auto 1fr; } /* Style a nav that contains the current page link */ nav:has(a[aria-current="page"]) { border-bottom: 2px solid var(--accent); }
Pattern 2

State-driven forms

React to input state without JavaScript.

/* Label turns accent when input has a value */ .field:has(input:not(:placeholder-shown)) .label { color: var(--accent); } /* Fieldset border highlights on focus */ fieldset:has(input:focus, textarea:focus) { border-left-color: var(--accent); } /* Submit button activates when all required fields filled */ form:has(.required-name:not(:placeholder-shown)):has(.required-email:not(:placeholder-shown)) .submit { opacity: 1; pointer-events: auto; }
Pattern 3

Sibling-based logic

Combine :has() with :not() and :checked for complex UI states.

/* Hide sibling content when a checkbox is checked */ .panel:has(input[type="checkbox"]:checked) .extra { display: none; } /* Dim list items that don't match a selected filter */ ul:has([data-tag="urgent"]) li:not(:has([data-tag="urgent"])) { opacity: 0.4; } /* Global layout change when sidebar is open */ body:has(#sidebar:popover-open) { overflow: hidden; }
Pattern 4

Real-world use cases

What :has() replaced three JavaScript patterns with one CSS rule.

/* Dark overlay when modal is open */ html:has(dialog[open])::before { content: ""; position: fixed; inset: 0; background: oklch(0% 0 0 / 0.6); } /* Sticky header when page has been scrolled */ body:has(.scroll-sentinel:not(:is(:first-child))) header { box-shadow: 0 4px 24px oklch(0% 0 0 / 0.3); } /* Grid layout changes based on number of items */ .grid:has(> :nth-child(4)) { grid-template-columns: repeat(4, 1fr); }

Query Builder

Build a :has() selector and watch the DOM respond in real-time.

Generated CSS
div:has(input:checked) { /* your styles here */ }
Live DOM — matching elements highlight Interact with the demo elements
<form>
<div>
<div>
<section>
<div class="active">
Active content Demo image
<div>
<ul>
<li>
<li>
Demo image