Using :has() as a CSS Parent Selector and much more

It’s been a long-standing dream of front-end developers to have a way to apply CSS to an element based on what’s happening inside that element.

Maybe we want to apply one layout to an article element if there’s a hero image at the top, and a different layout if there is no hero image. Or maybe we want to apply different styles to a form depending on the state of one of its input fields. How about giving a sidebar one background color if there’s a certain component in that sidebar, and a different background color if that component is not present? Use cases like these have been around for a long time, and web developers have repeatedly approached the CSS Working Group, begging them to invent a “parent selector”.

Over the last twenty years, the CSS Working Group discussed the possibility many, many times. The need was clear and well understood. Defining syntax was a doable task. But figuring out how a browser engine could handle potentially very complex circular patterns, and get through the calculations fast enough seemed impossible. Early versions of a parent selector were drafted for CSS3, only to be deferred. Finally, the :has() pseudo-class was officially defined in CSS Selectors level 4. But having a web standard alone didn’t make :has() a reality. We still needed a browser team to figure out the very real performance challenge. In the meantime, computers continued to get more powerful and faster year after year.

In 2021, Igalia started advocating for :has() among browser engineering teams, prototyping their ideas and documenting their findings regarding performance. The renewed attention on :has() caught the attention of engineers who work on WebKit at Apple. We started implementing the pseudo-class, thinking through possibilities for the needed performance enhancements to make this work. We debated whether to start with a faster version with a very limited and narrow scope of what it could do, and then try to remove those limits if possible… or to start with something that had no limits, and only apply restrictions as required. We went for it, and implemented the more powerful version. We developed a number of novel :has-specific caching and filtering optimizations, and leveraged the existing advanced optimization strategies of our CSS engine. And our approach worked, proving that after a two decade wait, it is finally possible to implement such a selector with fantastic performance, even in the presence of large DOM trees and large numbers of :has() selectors.

The WebKit team shipped :has() in Safari Technology Preview 137 in December 2021, and in Safari 15.4 on March 14, 2022. Igalia did the engineering work to implement :has() in Chromium, which will ship in Chrome 105 on August 30, 2022. Presumably the other browsers built on Chromium won’t be far behind. Mozilla is currently working on the Firefox implementation.

So, let’s take a step-by-step hands-on look at what web developers can do with this desperately desired tool. It turns out, the :has() pseudo-class is not just a “parent selector”. After decades of dead-ends, this selector can do far more.

The basics of how to use :has() as a parent selector

Let’s start with the basics. Imagine we want to style a <figure> element based on the kind of content in the figure. Sometimes our figure wraps only an image.

<figure>
  <img src="flowers.jpg" alt="spring flowers">
</figure>

While other times there’s an image with a caption.

<figure>
  <img src="dog.jpg" alt="black dog smiling in the sun">
  <figcaption>Maggie loves being outside off-leash.</figcaption>
</figure>

Now let’s apply some styles to the figure that will only apply if there is a figcaption inside the figure.

figure:has(figcaption) {
  background: white;
  padding: 0.6rem;
}

This selector means what it says — any figure element that has a figcaption inside will be selected.

Here’s the demo, if you’d like to alter the code and see what happens. Be sure to use a browser that supports :has() — as of today, that’s Safari.

See the Pen
:has() demo — Figure variations
by Jen Simmons (@jensimmons)
on CodePen.

In this demo, I also target any figure that contains a pre element by using figure:has(pre).

figure:has(pre) { 
  background: rgb(252, 232, 255);
  border: 3px solid white;
  padding: 1rem;
}

And I use a Selector Feature Query to hide a reminder about browser support whenever the current browser supports :has().

@supports selector(:has(img)) {
  small {
    display: none;
  }
}

The @supports selector() at-rule is itself very well supported. It can be incredibly useful anytime you want to use a feature query to test for browser support of a particular selector.

And finally, in this first demo, I also write a complex selector using the :not() pseudo-class. I want to apply display: flex to the figure — but only if an image is the sole content. Flexbox makes the image stretch to fill all available space.

I use a selector to target any figure that does not have any element that is not an image. If the figure has a figcaption, pre, p, or an h1 — or any element at all besides img — then the selector doesn’t apply.

figure:not(:has(:not(img))) {
  display: flex;
}

:has() is a powerful thing.

A practical example using :has() with CSS Grid

Let’s look at a second demo where I’ve used :has() as a parent selector to easily solve a very practical need.

I have several article teaser cards laid out using CSS Grid. Some cards contain only headlines and text, while others also have an image. I want the cards with images to take up more space on the grid than those without images.

I don’t want to have to do extra work to get my content management system to apply a class or to use JavaScript for layout. I just want to write a simple selector in CSS that will tell the browser to make any teaser card with an image to take up two rows and two columns in the grid.

The :has() pseudo-class makes this simple:

article:has(img) {
  grid-column: span 2;
  grid-row: span 2;
}

See the Pen
:has() demo — teaser cards
by Jen Simmons (@jensimmons)
on CodePen.

These first two demos use simple element selectors from the early days of CSS, but all of the selectors can be combined with :has(), including the class selector, the ID selector, the attribute selector — and powerful combinators.

Using :has() with the child combinator

First, a quick review of the difference between the descendant combinator and the child combinator (>).

The descendant combinator has been around since the very beginning of CSS. It’s the fancy name for when we put a space between two simple selectors. Like this:

a img { ... }

This targets all img elements that are contained within an a element, no matter how far apart the a and the img are in the HTML DOM tree.

<a>
  <figure>
    <img src="photo.jpg" alt="don't forget alt text" width="200" height="100">
  </figure>
</a>

Child combinator is the name for when we put an > between two selectors — which tells the browser to target anything that matches the second selector, but only when the second selector is a direct child of the first.

a > img { ... }

For example, this selector targets all img elements wrapped by an a element, but only when the img is immediately after the a in the HTML.

<a>
  <img src="photo.jpg" alt="don't forget alt text" width="200" height="100">
</a>

With that in mind, let’s consider the difference between the following two examples. Both select the a element, rather than the img, since we are using :has().

a:has(img) { ... }
a:has(> img) { ... }

The first selects any a element with an img inside — any place in the HTML structure. While the second selects an element only if the img is a direct child of the a.

Both can be useful; they accomplish different things.

See the Pen
:has() — descendant combinator vs child combinator
by Jen Simmons (@jensimmons)
on CodePen.

There are two additional types of combinators — both are siblings. And it’s through these that :has() becomes more than a parent selector.

Using :has() with sibling combinators

Let’s review the two selectors with sibling relationships. There’s the next-sibling combinator (+) and the subsequent-sibling combinator (~).

The next-sibling combinator (+) selects only the paragraphs that come directly after an h2 element.

h2 + p
<h2>Headline</h2>
<p>Paragraph that is selected by `h2 + p`, because it's directly after `h2`.</p>

The subsequent-sibling combinator (~) selects all paragraphs that come after an h2 element. They must be siblings, but there can be any number of other HTML elements in between.

h2 ~ p
<h2>Headline</h2>
<h3>Something else</h3>
<p>Paragraph that is selected by `h2 ~ p`.</p>
<p>This paragraph is also selected.</p>

Note that both h2 + p and h2 ~ p select the paragraph elements, and not the h2 headlines. Like other selectors (think of a img), it’s the last element listed that is targeted by the selector. But what if we want to target the h2? We can use sibling combinators with :has().

How often have you wanted to adjust the margins on a headline based on the element following it? Now it’s easy. This code allows us to select any h2 with a p immediately after it.

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

Amazing.

What if we want to do this for all six headline elements, without writing out six copies of the selector. We can use :is to simplify our code.

:is(h1, h2, h3, h4, h5, h6):has(+ p) { margin-bottom: 0; }

Or what if we want to write this code for more elements than just paragrapahs? Let’s eliminate the bottom margin of all headlines whenever they are followed by paragraphs, captions, code examples and lists.

:is(h1, h2, h3, h4, h5, h6):has(+ :is(p, figcaption, pre, dl, ul, ol)) { margin-bottom: 0; }

Combining :has() with descendant combinators, child combinators (>), next-sibling combinators (+), and subsequent-sibling combinators (~) opens up a world of possibilities. But oh, this is still just the beginning.

Styling form states without JS

There are a lot of fantastic pseudo-classes that can be used inside :has(). In fact, it revolutionizes what pseudo-classes can do. Previously, pseudo-classes were only used for styling an element based on a special state — or styling one of its children. Now, pseudo-classes can be used to capture state, without JavaScript, and style anything in the DOM based on that state.

Form input fields provide a powerful way to capture such a state. Form-specific pseudo-classes include :autofill, :enabled, :disabled, :read-only, :read-write, :placeholder-shown, :default, :checked, :indeterminate, :valid, :invalid, :in-range, :out-of-range, :required and :optional.

Let’s solve one of the use cases I described in the introduction — the long-standing need to style a form label based on the state of the input field. Let’s start with a basic form.

<form>
  <div>
    <label for="name">Name</label> 
    <input type="text" id="name">
  </div>
  <div>
    <label for="site">Website</label> 
    <input type="url" id="site">
  </div>
  <div>
    <label for="email">Email</label>
    <input type="email" id="email">
  </div>
</form>

I’d like to apply a background to the whole form whenever one of the fields is in focus.

form:has(:focus-visible) { 
  background: antiquewhite;
}

Now I could have used form:focus-within instead, but it would behave like form:has(:focus). The :focus pseudo-class always applies CSS whenever a field is in focus. The :focus-visible pseudo-class provides a reliable way to style a focus indicator only when the browser would draw one natively, using the same complex heuristics the browser uses to determine whether or not to apply a focus-ring.

Now, let’s imagine I want to style the other fields, the ones not in focus — changing their label text color and the input border color. Before :has(), this required JavaScript. Now we can use this CSS.

form:has(:focus-visible) div:has(input:not(:focus-visible)) label {
  color: peru;
}
form:has(:focus-visible) div:has(input:not(:focus-visible)) input {
  border: 2px solid peru;
}

What does that selector say? If one of the controls inside this form has focus, and the input element for this particular form control does not have focus, then change the color of this label’s text to peru. And change the border of the input field to be 2px solid peru.

You can see this code in action in the following demo by clicking inside one of the text fields. The background of the form changes, as I described earlier. And the label and input border colors of the fields that are not in focus also change.

See the Pen
:has() demo: Forms
by Jen Simmons (@jensimmons)
on CodePen.

In this same demo, I would also like to improve the warning to the user when there’s an error in how they filled out the form. For years, we’ve been able to easily put a red box around an invalid input with this CSS.

input:invalid {
  outline: 4px solid red;
  border: 2px solid red;
} 

Now with :has(), we can turn the label text red as well:

div:has(input:invalid) label {
  color: red;
}

You can see the result by typing something in the website or email field that’s not a fully-formed URL or email address. Both are invalid, and so both will trigger a red border and red label, with an “X”.

Dark mode toggle with no JS

And last, in this same demo I’m using a checkbox to allow the user to toggle between a light and dark theme.

body:has(input[type="checkbox"]:checked) {
  background: blue;
  --primary-color: white;
}
body:has(input[type="checkbox"]:checked) form { 
  border: 4px solid white;
}
body:has(input[type="checkbox"]:checked) form:has(:focus-visible) {
  background: navy;
}
body:has(input[type="checkbox"]:checked) input:focus-visible {
  outline: 4px solid lightsalmon;
}

I’ve styled the dark mode checkbox using custom styles, but it does still look like a checkbox. With more complex styles, I could create a toggle in CSS.

In a similar fashion, I could use a select menu to provide a user with multiple themes for my site.

body:has(option[value="pony"]:checked) {
  --font-family: cursive;
  --text-color: #b10267;
  --body-background: #ee458e;
  --main-background: #f4b6d2;
}

See the Pen
:has() Demo #5 — Theme picker via Select
by Jen Simmons (@jensimmons)
on CodePen.

Any time there’s an opportunity to use CSS instead of JavaScript, I’ll take it. This results in a faster experience and a more robust website. JavaScript can do amazing things, and we should use it when it’s the right tool for the job. But if we can accomplish the same result in HTML and CSS alone, that’s even better.

And more

Looking through other pseudo-classes, there are so many that can be combined with :has(). Imagine the possibilities with :nth-child, :nth-last-child, :first-child, :last-child, :only-child, :nth-of-type, :nth-last-of-type, :first-of-type, :last-of-type, :only-of-type. The brand new :modal pseudo-class is triggered when a dialog is in the open state. With :has(:modal) you can style anything in the DOM based on whether the dialog is open or closed.

However, not every pseudo-class is currently supported inside :has() in every browser, so do try out your code in multiple browsers. Currently the dynamic media pseudo-classes don’t work — like :playing, :paused, :muted, etc. They very well may work in the future, so if you are reading this in the future, test them out! Also, form invalidation support is currently missing in certain specific situations, so dynamic state changes to those pseudo-classes may not update with :has().

Safari 16 will add support for :has(:target) opening up interesting possibilities for writing code that looks at the current URL for a fragment that matches the ID of a specific element. For example, if a user clicks on a table of contents at the top of a document, and jumps down to the section of the page matching that link, :target provides a way to style that content uniquely, based on the fact the user clicked the link to get there. And :has() opens up what such styling can do.

Something to note — the CSS Working Group resolved to disallow all existing pseudo-elements inside of :has(). For example, article:has(p::first-line) and ol:has(li::marker) won’t work. Same with ::before and ::after.

The :has() revolution

This feels like a revolution in how we will write CSS selectors, opening up a world of possibilities previously either impossible or often not worth the effort. It feels like while we might recognize immediately how useful :has() will be, we also have no idea what is truly possible. Over the next several years, people who make demos and dive deep into what CSS can do will come up with amazing ideas, stretching :has() to its limits.

Michelle Barker created a fantastic demo that triggers the animation of Grid track sizes through the use of :has() and hover states. Read more about it in her blog post. Support for animated grid tracks will ship in Safari 16. You can try out this demo today in Safari Technology Preview or Safari 16 beta.

The hardest part of :has() will be opening our minds to its possibilities. We’ve become so used to the limits imposed on us by not having a parent selector. Now, we have to break those habits.

That’s all the more reason to use vanilla CSS, and not limit yourself to the classes defined in a framework. By writing your own CSS, custom for your project, you can fully leverage all the powerful abilities of today’s browsers.

What will you use :has() for? Last December, I asked on Twitter what use cases folks might have for :has(), and got lots of replies with incredible ideas. I can’t wait to see yours.