DEV Community

Cover image for CSS Parent & Previous-Sibling Selectors are here!
Bryce Howitson
Bryce Howitson

Posted on

CSS Parent & Previous-Sibling Selectors are here!

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>
Enter fullscreen mode Exit fullscreen mode
ul {
   display: none;
   /* hide all the unordered lists */
}
ul:has( > li) {
   display: block;
   /* show unordered lists with a direct child of li */ 
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

With the defaults, you'll get this form with an extension input that wraps. That's not great.

Two input fields side by side

Three inputs where the 3rd wraps to another line

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>
Enter fullscreen mode Exit fullscreen mode

Now we get this:

3 inputs without wrapping and dynamic sizes

Top comments (0)