:has() Advanced Patterns
The :has() introduction covers the core idea — matching a parent based on what's inside it. Once that clicks, :has() turns out to be flexible enough to fake several selectors CSS has never actually had. This page walks through a few of the more advanced, creative patterns it enables.
Pattern 1: styling a label from its checkbox's checked state
Custom toggle switches and styled checkboxes traditionally rely on the sibling combinator (
input:checked + label), which only works when the label immediately follows the input in the markup. :has() removes that markup constraint — the label can wrap the input, or the relationship can be expressed however makes sense for the component.CSS
.toggle {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.4rem 0.75rem;
border: 1px solid #ccc;
border-radius: 999px;
cursor: pointer;
}
/* Style the label wrapper itself based on its own checkbox's state */
.toggle:has(input:checked) {
background: #0066cc;
color: white;
border-color: #0066cc;
}
.toggle input {
/* visually hide the native checkbox but keep it accessible */
position: absolute;
opacity: 0;
}HTML
<label class="toggle"> <input type="checkbox" /> Notifications </label>
Pattern 2: quantity-based styling
Combining :has() with :nth-child() lets you style a container differently depending on how many children it has — for example, switching a list's layout once it grows past a threshold, without any JavaScript counting logic.
CSS
/* Default: a simple stacked list */
.item-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
/* If there's a 3rd child, we know there are 3+ items —
switch to a denser grid layout */
.item-list:has(> :nth-child(3)) {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 0.75rem;
}
/* A distinct empty-state style when there are no items at all */
.item-list:not(:has(> *)) {
padding: 2rem;
text-align: center;
color: #888;
}Pattern 3: simulating a "previous sibling" selector
CSS has combinators for the next sibling (
+, ~) but has never had a true previous-sibling selector — there is no way to say "the element immediately before this one." :has() combined with :not() can approximate certain previous-sibling-like conditions, by testing candidates for whether they are immediately followed by the target rather than testing the target for what precedes it.CSS
/* Goal: style a heading whenever it's immediately followed
by an element with the class .highlight — i.e. style the
PRECEDING sibling based on what comes after it */
h3:has(+ .highlight) {
color: #cc6600;
border-bottom: 2px solid #cc6600;
}CSS
/* A trickier version: style every list item EXCEPT the one
right before the currently ".active" item, by first finding
"items immediately followed by .active" and excluding them */
.item {
opacity: 1;
}
.item:has(+ .active) {
opacity: 0.6; /* the item right before the active one, dimmed */
}Note
This is a workaround built out of :has() plus adjacency, not a literal previous-sibling selector — it only works when the condition can be expressed relative to what follows an element. It cannot answer arbitrary "what's two elements before this one" questions the way a real previous-sibling combinator eventually might.
Pattern 4: dimming everything except a hovered card
CSS
/* When the grid contains a hovered card, dim every card that
is NOT itself the one being hovered */
.grid:has(.card:hover) .card:not(:hover) {
opacity: 0.5;
transition: opacity 0.2s ease;
}.list:has(> li:only-child)— style a list only when it has exactly one item.fieldset:has(:invalid:focus)— highlight a whole fieldset only while an invalid field inside it is actively focused.body:has(.modal[open])— a common pattern for locking page scroll while any modal is open, regardless of which modal.
Note
:has() is genuinely one of the most creatively powerful additions to modern CSS. Because the selector inside it can be arbitrarily complex and combined with any other selector, most "I wish CSS could just look at X and style Y" requests from the last decade turn out to have a :has()-based answer today.
Next
Blend, derive and theme colors with modern functions: [Modern Color Functions](/css/color-functions).