Now that I’ve got your attention, no, we didn’t get any sexy new selectors to get a parent or previous sibling and we might never get those. But, good support for the :has()
selector provides a lot of opportunity.
Select direct parent
TLDR: parent:has( > child) { ... }
While we don’t have a parent selector, we DO have a direct child combinator using the greater than >
symbol. If we combine the direct child selector with :has()
the parent is selectable from the other direction.
Ideally, we would use specific selectors like a class or ID for the parent, but what if you don’t have a class to use? You can select against HTML elements just as easily. div:has( > ul)
would select any div that contains an unordered list. Ok fine. You’re stuck and the parent you’re trying to select isn’t even the same element all the time (thanks dynamically generated DOM trees). In this case the CSS Universal Selector *
has you covered. In these cases, while not computationally efficient, you can filter the entire list of elements. *:has( > .childElement)
Select previous sibling
TLDR: previousSibling:has( + directSibling) { ... }
Similar to the direct child combinator, we can use :has()
along with the next-sibling combinator +
to find elements directly followed by a known element. For example, img:has( + caption)
would select all <img>
elements directly followed by a <caption>
.
Also similar to the previous example, it’s possible to use the CSS Universal Selector in conjunction with :has()
for those times you don’t know the previous sibling element. *:has( + .nextElement)
You probably don’t need it
Let’s be honest, in most cases, you don’t really need to select a parent or previous sibling. Often you have access to whatever system generates the DOM including the ability to remove empty elements, add classes as needed, or change the structure based on data. This is likely why versions of these selectors haven’t been prioritized.
Sometimes you can’t change the DOM
However, on occasion, you’re stuck with whatever DOM a system spits out. You might have to contend with empty lists. You might see elements that sometimes exist and sometimes don’t. You may even be building for a CMS where you don’t always control the output. Yet you still need to style those elements. In these cases, :has()
becomes incredibly useful.
Enough theory give me examples
Example 1
You have a <ul>...</ul>
that sometimes has items and sometimes doesn’t. You want to hide the list when it’s empty so it doesn’t break flexbox
or grid
layouts.
<ul></ul> <!-- this one will break your layout -->
<ul>
<li>List item</li>
<li>List item</li>
</ul>
<ul>
<li>List item</li>
<li>List item</li>
<li>List item</li>
</ul>
ul {
display: none;
/* hide all the unordered lists */
}
ul:has( > li) {
display: block;
/* show unordered lists with a direct child of li */
}
Example 2
A dynamically generated contact form that collects a phone number, but sometimes allows an extension in a separate input. In this case, you don’t want to break the layout with an extra full-sized input.
<!-- Version A -->
<form>
<input type="text" placeholder="Your Name">
<input type="tel" placeholder="Your Number">
</form>
<!--Version B -->
<form>
<input type="text" placeholder="Your Name">
<input type="tel" placeholder="Your Number">
<input type="text" placeholder="Ext" class="extension">
</form>
<style>
/* Form is a grid with 6 columns */
form {
display: grid;
grid-template-columns: repeat(1fr, 6);
gap: 1rem;
}
/* inputs take 3 columns by default */
input {
grid-column: span 3;
}
</style>
With the defaults, you’ll get this form with an extension input that wraps. That’s not great.
But if we use :has()
to control the phone input and the next-sibling combinator to select the “extension” it doesn’t have to wrap. Now, all inputs are on one line, similar to the form without the extension.
<!-- Version A -->
<form>
<input type="text" placeholder="Your Name">
<input type="tel" placeholder="Your Number">
</form>
<!--Version B -->
<form>
<input type="text" placeholder="Your Name">
<input type="tel" placeholder="Your Number">
<input type="text" placeholder="Ext" class="extension">
</form>
<style>
/* Form is a grid with 6 columns */
form {
display: grid;
grid-template-columns: repeat(1fr, 6);
gap: 1rem;
}
/* inputs take 3 columns by default */
input {
grid-column: span 3;
}
/* select any input of type tel that's followed directly by .extension
use input type tel in case someone decides to use .extension on something else
Make the input 2 columns wide */
input[type=tel]:has( + .extension) {
grid-column: span 2;
}
/* select any class .extension directly following input type tel
make the following input only one column wide */
input[type=tel] + .extension {
grid-column: span 1;
}
</style>
Now we get this: