For most of CSS's life, selectors only went one way: from a parent down to its children. You could style the image inside a card, but not the card because it had an image. That took JavaScript.

:has() fixed it. It matches an element based on what is inside it, which makes it the parent selector people asked for over many years. Every current browser supports it.

The CSS has child selector

Put the child inside :has(), and the rule applies to the element in front of it:

/* the card gets the border, not the image */
.card:has(img) {
  border: 2px solid #2563eb;
}

Two forms matter, and mixing them up is the most common bug:

SelectorMatches
.card:has(img)Cards with an image anywhere inside, at any depth
.card:has(> img)Cards where the image is a direct child
.card:has(.badge.new)Cards containing an element with both classes
.card:not(:has(img))Cards with no image at all

Select a parent from a child in CSS

A few places where :has() replaces script we used to write:

Highlight a form field that has an error

.field:has(input:invalid) label {
  color: #b91c1c;
}

The label turns red while the input inside the same .field is invalid. No event listener needed.

Change the layout when a card has an image

.card:has(> img) {
  display: grid;
  grid-template-columns: 160px 1fr;
}

Combine conditions

Chaining two :has() calls means "has both". A comma inside one means "has either":

/* has a video AND a caption */
.tile:has(video):has(.caption) { padding-bottom: 2rem; }

/* has a video OR an image */
.tile:has(video, img) { background: #000; }

We use the first pattern in video call layouts: a participant tile that currently has a <video> element shows it, and a tile without one shows the name and avatar. It saves a class toggle every time a camera turns on or off. For the other half of that UI, hiding HTML5 video controls covers the video element itself.

CSS select all children

Many people searching for "css has children" actually want the opposite: style the children, not the parent. That doesn't need :has() at all:

.list > * { margin-bottom: 8px; }   /* every direct child */
.list * { font-family: inherit; }   /* every descendant */
.list > li:first-child { font-weight: 700; }

Rule of thumb: if the style lands on the children, use > or a space. If the style lands on the container, and depends on the children, use :has().

Browser support

:has() works in Safari 15.4+, Chrome and Edge 105+, and Firefox 121+. caniuse has the current numbers, and MDN's :has() reference lists the edge cases. If an old browser must keep working, write the default look first and add the :has() rule on top, so the page still makes sense without it.

Check support in the stylesheet itself

@supports selector(:has(a)) lets you wrap rules that only make sense when :has() works, which is cleaner than detecting it in JavaScript.

Things that trip people up

  • You can't nest :has() inside :has(). The browser rejects the whole selector.
  • A broad selector on body is slow. body:has(.modal-open) is fine. *:has(*) is not.
  • Specificity comes from the argument. .card:has(#hero) carries the weight of an ID selector, so it can override rules you didn't expect.

If you are working on layout, truncating text with three dots and the building blocks of responsive design are the other two problems that come up in almost every component.

Frequently Asked Questions

Is there a CSS has child selector?

Yes: the :has() pseudo-class. div:has(img) matches every div that contains an img anywhere inside it. div:has(> img) matches only divs where the img is a direct child.

How do I select a parent from a child in CSS?

Put the child in :has() on the parent. For example, .card:has(.badge) styles the card, not the badge. Before :has() this was impossible in CSS and needed JavaScript.

How do I select all children in CSS?

Use the child combinator with the universal selector: .list > * selects every direct child of .list. .list * selects every descendant at any depth. These style the children; :has() is for styling the parent.

Which browsers support :has()?

All current major browsers. Safari shipped it in 15.4, Chrome and Edge in 105, and Firefox in 121 at the end of 2023. Only very old browsers ignore it, so give those a sensible default style.

Does :has() hurt performance?

Usually not in a normal page. Keep the argument specific, such as :has(> .error) rather than :has(*), and avoid putting it on body or html with a broad selector, because the browser has to recheck it on every change.