Parent and previous-sibling selectors
For years "CSS can't select a parent" was a rule of the web. It isn't true any more: the native :has() pseudo-class does it, and every current browser has supported it since December 2023 (Chrome 105, Safari 15.4, Firefox 121). This page shows the patterns, then what css-pp adds on top.
Select a parent element#
A:has(B) selects every A that contains a B:
/* cards that contain an image */
.card:has(img) { padding: 0; }
/* the direct parent of .child, only one level up */
.list:has(> .child) { outline: 1px solid; }Don't put quotes inside :has(). It takes a selector, not a string.
Select the parent when you don't know what it is#
Leave out the part before :has(). The selector then matches whatever element is the direct parent of .child, wherever .child is moved:
:has(> .child) { background: #fef9c3; }Keep the >. Without it, :has(.child) matches every ancestor of .child, all the way up to <html>.
For the grandparent, chain child combinators inside a single :has(), because :has() can't be nested inside another :has():
:has(> * > .child) { border: 1px dashed; }Select previous siblings#
Sibling combinators only look forward: .child ~ li selects the siblings after .child. To go backward, put the combinator inside :has():
/* every sibling BEFORE .current (the 1st and 2nd when .current is 3rd) */
li:has(~ .current) { color: #16a34a; }
/* only the sibling immediately before .current */
li:has(+ .current) { font-weight: 600; }
/* all siblings of .current, before and after */
:has(> .current) > :not(.current) { opacity: .6; }What :has() can't see, and how css-pp adds it#
:has() can only test things selectors can express: elements, classes, attributes and native states like :checked. It can't ask whether a child is on screen, cut off, contains some text, or has a number above a threshold. css-pp's selectors compile to plain attribute selectors, so they work inside :has() and give you parent selection based on all of these:
/* parent of an element that has been scrolled into view */
.timeline:has(> .event:seen) { --line: 100%; }
/* parent of an element whose text is truncated */
.post:has(.excerpt:overflowing) .more { display: inline; }
/* parent of an element with specific text */
.product:has(.badge:contains("sold out" i)) { opacity: .5; }
/* parent of an element with a number below a threshold */
.cart:has([data-qty>9]) .bulk-discount { display: block; }
/* the page header, once the hero has left the screen */
body:has(.hero:not(:in-view)) header { background: #fff; }See state selectors and content selectors for the full syntax.
Quick reference#
| Goal | Selector |
|---|---|
Parent of .child, known type |
.list:has(> .child) |
Parent of .child, any element |
:has(> .child) |
Any ancestor of .child |
:has(.child) |
Grandparent of .child |
:has(> * > .child) |
Siblings before .child |
:has(~ .child) |
Sibling right before .child |
:has(+ .child) |
| Parent of a visible child (css-pp) | :has(> .child:in-view) |
| Parent of a truncated child (css-pp) | :has(> .child:overflowing) |