# The CSS :has() selector

> The CSS parent selector styles an element from what is inside it. Forms, cards, previous siblings, quantity queries, and when JavaScript is still better.

Author: [Flavio Copes](https://flaviocopes.com/about/) | Published: 2026-08-17 | Updated: 2026-08-16 | Topics: [CSS](https://flaviocopes.com/tags/css/) | Canonical: https://flaviocopes.com/css-has-selector/

For years CSS could only look down the tree. You styled a child. You styled a next sibling. You could not style a parent because of what was inside it.

So we added classes with JavaScript. `is-invalid` on the field wrapper. `has-image` on the card. `is-checked` on the label. The HTML grew flags that only existed to please CSS.

`:has()` is the parent selector. You keep the meaning in the markup. The browser looks inside, or next to, an element, and styles that element.

```css
.field:has(input:user-invalid) {
  border-color: red;
}
```

The wrapper turns red because the input inside it failed validation. No extra class. No script.

I use it anywhere I used to toggle a class on a parent. I still use JavaScript when the page has to *do* something, not just look different.

## How :has() thinks

The selector is `anchor:has(relative)`.

The **anchor** is the element you style. The part inside `:has()` is a relative selector, starting from that anchor. If anything in there matches, the anchor matches.

```css
article:has(img) {
  padding: 0;
}
```

"Select `article` if it contains an `img`." The style goes on the article, not on the image.

A child combinator keeps the search shallow:

```css
article:has(> img) {
  padding: 0;
}
```

Now the image must be a direct child. Faster to think about. Faster for the browser too, on a big tree.

`:has()` also looks *forward* at siblings. That is the other trick people wanted for years: style an element based on what comes after it.

```css
h1:has(+ h2) {
  margin-bottom: 0.25rem;
}
```

If an `h1` is immediately followed by an `h2`, tighten the gap. You are styling the heading that came *first*. Old CSS could not do that. The next-sibling combinator only styles the second element.

You can try selectors like this in the [CSS selector tester](https://flaviocopes.com/tools/css-selector-tester/) if you want to see a match light up on a sample tree.

## Highlight a form field that failed

A label wrapping an input is the example I reach for first.

```html
<form class="signup">
  <label class="field">
    Email
    <input type="email" name="email" required>
  </label>
  <button type="submit">Subscribe</button>
</form>
```

```css
.field {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  border: 1px solid #ccc;
  padding: 0.75rem;
}

.field:has(input:user-invalid) {
  border-color: red;
}

.field:has(input:user-valid) {
  border-color: green;
}
```

`:invalid` is true as soon as the page loads, because an empty required field is invalid. That paints every required field red before the reader types. `:user-invalid` waits until the person interacted with the field, or tried to submit.

This sits on top of normal [HTML form](https://flaviocopes.com/html-forms/) validation. The browser already knows the field is wrong. You are only dressing the wrapper.

You can lift the same idea to the whole form:

```css
.signup:has(:user-invalid) {
  box-shadow: 0 0 0 1px red;
}
```

I would keep that light. A red ring on the form is enough. Do not hide the submit button just because one field is empty. The reader still needs a way to try.

## Cards that change when they contain something

A card with an image wants different padding than a text-only card.

```html
<article class="card">
  <img src="/images/css-grid.jpg" alt="">
  <div class="card-body">
    <h2>CSS Grid</h2>
    <p>Two-dimensional layout without hacks.</p>
  </div>
</article>

<article class="card">
  <div class="card-body">
    <h2>CSS Units</h2>
    <p>px, rem, em, and when to use each.</p>
  </div>
</article>
```

```css
.card {
  padding: 1rem;
}

.card:has(img) {
  padding: 0;
}

.card:has(img) .card-body {
  padding: 1rem;
}
```

Cards without an image keep their padding. Cards with an image go edge to edge, and the body gets the inset.

The same pattern works for a featured flag in the HTML:

```css
section:has(.featured) {
  border: 2px solid blue;
}
```

You style the section because one article inside it is featured. The featured article can keep its own styles. The parent reacts.

This is the DOM half of responsive cards. [Container queries](https://flaviocopes.com/css-container-queries/) are the space half. `:has(img)` asks what is inside. `@container` asks how wide the slot is. A wide card with an image is a different layout from a narrow text card.

## Style a label from a checkbox

`:has()` works with other [pseudo-classes](https://flaviocopes.com/css-pseudo-classes/).

```html
<label class="option">
  <input type="checkbox" name="topics" value="css">
  CSS
</label>
```

```css
.option {
  display: flex;
  gap: 0.5rem;
  padding: 0.5rem 0.75rem;
}

.option:has(input:checked) {
  background: #e8f5e9;
  font-weight: bold;
}

.option:has(input:disabled) {
  opacity: 0.5;
}
```

The label reacts to the control inside it. You do not need the old `input + span` trick, and you do not need the checkbox to be a sibling of the text.

Radio groups work the same way. So do `select` elements:

```css
.field:has(select:required:invalid) {
  border-color: red;
}
```

## Style the previous sibling

This is the one that still feels like a magic trick.

```css
h1:has(+ p) {
  margin-bottom: 0.5rem;
}

figure:has(figcaption) img {
  margin-bottom: 0.25rem;
}

li:has(+ li) {
  border-bottom: 1px solid #ddd;
}
```

`h1:has(+ p)` styles the heading when a paragraph follows it. `li:has(+ li)` styles every item that is not the last one, because a next `li` exists.

You can also look further:

```css
h2:has(~ h2) {
  /* this h2 is not the last h2 in the block */
}
```

`+` is the next sibling. `~` is any later sibling.

I use this for heading spacing more than anything else. Two headings in a row should sit closer than a heading followed by a paragraph. That used to be a special class or a `:has()`-less compromise on the second heading.

## Quantity queries

`:has()` plus `:nth-child` lets you style a list based on how many items it has.

```css
.tags:has(> * :nth-child(2)) {
  /* at least 2 tags */
}

.tags:has(> * :nth-child(5)) {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
}

.gallery:has(> img:only-child) {
  max-width: 28rem;
}
```

`:has(> * :nth-child(5))` is true when a fifth child exists, so the list has at least five items. A short tag list can stay inline. A long one wraps.

A gallery with one image should not stretch like a five-image grid. `:only-child` says that without counting in JavaScript.

## AND versus OR

A comma inside `:has()` is OR. Chaining `:has()` is AND.

```css
body:has(video, audio) {
  /* the page has a video OR an audio player */
}

body:has(video):has(audio) {
  /* the page has both */
}

.card:has(img):has(figcaption) {
  /* image and caption */
}
```

I almost never put `:has()` on `body`. The example is only here to show the logic. Anchor it to a smaller element. More on that in a minute.

`:is()` keeps long lists readable:

```css
:is(h1, h2, h3):has(+ :is(h2, h3, h4)) {
  margin-bottom: 0.25rem;
}
```

Any of those headings, followed by a slightly smaller heading, gets a tighter margin.

## Combining with :not()

Exclude the cases you do not want:

```css
.nav:has(a.active):not(:has(.dropdown)) {
  border-bottom: 2px solid blue;
}

.card:not(:has(img)) {
  background: #f6f6f6;
}
```

The first rule styles a nav that has an active link, but only when there is no dropdown inside. The second is often clearer than inventing a `card--text-only` class.

`.card:not(:has(img))` and `.card:has(img)` are a good pair. One path for text cards, one path for image cards. Nothing in between.

## Specificity

`:has()` does not add its own specificity. It takes the specificity of the most specific selector in its argument, the same way `:is()` and `:not()` do.

```css
.card {
  /* 0, 1, 0 */
}

.card:has(img) {
  /* 0, 1, 1  (.card + img) */
}

.card:has(.hero-image) {
  /* 0, 2, 0  (.card + .hero-image) */
}
```

`.card:has(.hero-image)` beats `.card:has(img)`. If a rule "does not apply," check whether a more specific `:has()` won. The [CSS specificity](https://flaviocopes.com/css-specificity/) guide is the longer version of this.

Because the argument counts, a sloppy `:has()` can become a specificity hammer:

```css
.card:has(#intro img.hero) {
  /* 1, 2, 1  — this will bully a lot of other rules */
}
```

Keep the inside simple. A class or an element is enough.

## What :has() cannot do

You cannot nest `:has()` inside `:has()`. This is invalid:

```css
article:has(div:has(img)) {
  /* no */
}
```

Write the descendant you mean:

```css
article:has(img) {
  /* yes */
}
```

Pseudo-elements are not allowed inside `:has()`, and `:has()` cannot hang off a pseudo-element.

```css
article:has(::before) {
  /* no */
}

article::before:has(img) {
  /* no */
}
```

The reason is cycles. `::before` exists because of styles. If styles could ask whether `::before` exists, the browser would chase its own tail.

If a browser does not support `:has()`, the whole selector is dropped. The rest of the rule does not apply. Wrap a fallback with a [feature query](https://flaviocopes.com/css-feature-queries/) when the layout would break without it:

```css
.card {
  padding: 1rem;
}

@supports selector(:has(*)) {
  .card:has(img) {
    padding: 0;
  }
}
```

For current Chrome, Firefox, and Safari you do not need that. I add `@supports` when the unstyled state would be wrong, not just less fancy.

## Do not put it on the whole page

`:has()` is not free. On every DOM change, the browser has to ask: does this still match?

If the anchor is `body`, `:root`, or `*`, every mutation can walk a huge tree.

```css
/* don't */
body:has(.sidebar-open) {
  overflow: hidden;
}

*:has(.item) {
  color: red;
}
```

Anchor to the element that actually changes:

```css
.layout:has(> .sidebar-open) {
  overflow: hidden;
}

.gallery:has(> img[data-loaded='false']) {
  opacity: 0.7;
}
```

Prefer `>` and `+` inside `:has()` when you can. `.card:has(> img)` checks one level. `.card:has(img)` may walk everything inside the card.

Use `:has()` for local UI. A field, a card, a nav, a list. If you need a page-wide mode — "the mobile menu is open, lock the body" — a single class on `body` from a click handler is still the honest solution. One class. No tree walk on every keystroke.

## How I would use this

I would use `:has()` on forms first.

The newsletter form on `/access` and the waiting-list forms on the course pages are labels plus inputs. A red border on `.field` when the email is `:user-invalid` is exactly the old `is-invalid` class, without the class.

I would use it on cards that optionally have an image or a "featured" mark. This blog's posts are mostly text. When a card does include an image, I want the padding to collapse without a `has-image` modifier in the HTML.

I would use `h1:has(+ h2)` and `h2:has(+ h3)` in long articles. Heading pairs should sit tighter than a heading plus a paragraph. That is a spacing bug I have fixed with extra classes too many times.

I would not use it to replace application state. Opening a dialog, submitting a form, filtering a list: those are still JavaScript. A dialog element and a click handler do that job. `:has()` can style the page *around* an open dialog if you want, but it should not be the thing that opens it.

I would not write `body:has(...)` to theme the whole site from a checkbox in the footer. It works in a demo. It is a performance footgun on a real page.

The [free CSS course](https://flaviocopes.com/courses/css/) covers selectors and pseudo-classes before you get here. `:has()` is easier once `:hover`, `:checked`, and `:not()` already feel normal.

## Browser support

`:has()` has been in all modern browsers since December 2023. It is safe in production now.

Safari was last. If you still see a screenshot from 2022 saying "Chrome only," that page is outdated.

My advice: use `:has()` when the only reason you opened JavaScript was to add a class on a parent. Keep the state in the HTML. Let CSS read it. Reach for a script when the user action has to change more than styles.
