# CSS parent selector and previous sibling selector: :has() explained

> How to select a parent element in CSS with :has(), target the parent of any element, select previous siblings, and style parents by visibility or text.

Source: https://csspp.dev/docs/parent-selector/


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](/docs/state-selectors/) and [content selectors](/docs/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)` |
