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:

CSS
/* 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:

CSS
: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():

CSS
: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():

CSS
/* 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:

CSS
/* 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)