albert.walickihire me
← all solutions
css

Is there a CSS parent selector?

Yes, CSS has a parent selector: :has(). See practical examples for cards, checked labels, invalid forms and previous siblings, plus its limits.

Yes. The :has() pseudo-class selects an element based on what it contains, and it's supported in all major browsers. .card:has(img) selects every .card that has an image somewhere inside it:

.card:has(img) {
  display: grid;
  grid-template-columns: 8rem 1fr;
}

The element before :has() is the one that gets styled. The selector inside the parentheses is only the condition. Cards without an image keep their normal layout, without an extra .card--with-image class.

:has() has been supported in Chrome, Edge, Safari and Firefox since the end of 2023. If you still support older browsers, check Can I Use.

Style a label when its checkbox is checked

label:has(input:checked) {
  background-color: #eef2ff;
  color: #3730a3;
}
<label>
  <input type="checkbox" name="extras" value="gift-wrap">
  Gift wrap
</label>

This is great for selectable cards and option lists. Before :has(), you could only style elements that came after the input, with selectors like input:checked + .box.

Style part of a form when a field is invalid

form:has(:invalid) .submit {
  background-color: #9ca3af;
}

This selects the .submit button inside any form that contains an invalid field. Note that it only changes the look. The button still works, and the browser's own validation still runs when the form is submitted.

Required fields that are still empty count as :invalid from the start, so the button looks inactive before the user has typed anything. If that's not what you want, use :user-invalid, which only matches after the user has interacted with the field. It's newer, so check its support.

Select the previous sibling

CSS can select the next sibling with +, but for a long time it had no way to select the previous one. :has() covers that too:

h2:has(+ p) {
  margin-bottom: 0.5rem;
}

This selects every h2 that is directly followed by a p, so a heading sits closer to the paragraph under it. The same idea lets you change one element when you hover the one after it; see how to change another element on hover.

Direct children only

:has(img) matches an image at any depth. Add > to only check direct children:

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

Limits

  • You can't nest :has() inside another :has().
  • Pseudo-elements aren't allowed inside it, so :has(::before) doesn't work.
  • A broad selector like body:has(.modal-open) works, but the browser has to check more of the page on every change. Anchor :has() to the nearest element that makes sense.

The old way: toggle a class with JavaScript

Before :has(), the only way was to add a class to the parent from JavaScript and style that class:

const checkbox = document.querySelector('#gift-wrap');

checkbox.addEventListener('change', () => {
  const label = checkbox.closest('label');
  label.classList.toggle('is-checked', checkbox.checked);
});

You only need this today if you support browsers older than the ones listed above. You can keep both versions and give old browsers the fallback with @supports not selector(:has(a)). How to change an element's class with JavaScript covers the class methods.

more solutions
work with me

Got something that needs building?

Frontend builds, full-stack features in Django, design-system work. Available for work.

See my workGet in touch