DEV Community

Diana Le
Diana Le

Posted on

CSS Selectors: Introduction to :has()

The functionality of CSS's new :has() selector has been something that I've wished existed natively ever since I first started learning web development. With :has(), you are able to style a parent element based on its children, or a previous sibling element based on its subsequent siblings. Before :has(), you could style an element in CSS if it followed another element, but in order to style a preceding element, you'd have to either flip the order of elements in the HTML, or resort to JavaScript. Now we can use pure CSS to style any element based on its relative selectors.

Note: At the time of this writing, this feature is supported in Chrome, Edge, and Safari. Firefox requires enabling the layout.css.has-selector.enabled flag, which I found still didn't give the full functionality.

Let's learn this selector with two simple examples, one utilizing siblings and the other utilizing parents.

Required Form Fields with Matching Labels

I've built and edited a lot of HTML forms, and when form fields were updated very frequently, one of the easiest mistakes to make was forgetting to indicate via the form label that the input was required (or was now optional after previously being required). The visual corrections were always easy to adjust (adding/removing an asterisk or "required" text to the label), but it would have been nice if there were a way to automatically sync the two together to reduce developer error. With :has(), now there is.

This is a standard form with a <div> containing each label and input. The label and input are sibling elements. The name and email fields are both required. For a required input, before :has(), I would write the HTML like so:

<div class="contact__field">
  <label for="name">Name <span>*</span></label>
  <input type="text" id="name" required>
</div>
Enter fullscreen mode Exit fullscreen mode

Since the "name" input is required, I would add a <span> tag containing an asterisk to the label to visually indicate to the user that this is a required field. This is where developer error occurs if this field is later switched to optional with the removal of the required attribute from the <input>, but where the label is not updated to match.

Using :has() to dynamically update required labels

With :has(), now we can target labels that specifically have any sibling with the "required" attribute. Just put the regular CSS selectors you want within the parentheses. In this example, I am using the adjacent sibling selector:

label:has(+ :required)::after {
  content: ' *';
  color: #f68ea6;
}
Enter fullscreen mode Exit fullscreen mode

This selector says, "Find all labels that have an immediate sibling with the 'required' attribute, and create the 'after' pseudo-element."

This means my HTML can now be updated. I no longer need to manually mark up the label with any additional HTML or classes because the CSS will now generate the asterisk for the label automatically depending on the existence of the "required" attribute of the <input> tag:

<div class="contact__field">
  <label for="name">Name</label>
  <input type="text" id="name" required>
</div>
Enter fullscreen mode Exit fullscreen mode

And that's it! If you follow the same HTML structure, you can add additional form fields and the label will adjust dynamically based on a required or optional input.

Navigation Menu with Parent Dropdowns

Let's say you have a primary navigation, and some of the landing pages have children pages, and these pages will be displayed once the user hovers over the parent link. You'd probably want an indicator to let the user know that there will be subpages, and we can do that with :has(). Here I am adding an arrow pseudo-element to links with child pages (in this example, the "About Us" link):

If you were using a CMS or components to build your site, you could use logic to mark parent pages with a specific class and then style from there, but I'm purposely avoiding using CSS classes so you can see how this can be done without additional markup.

For this menu I am using unordered lists, and for nested lists, the HTML syntax is:

<li><a href="#">About Us</a>
  <ul>
   <li><a href="#">Culture</a></li>
   <li><a href="#">History</a></li>
   <li><a href="#">Team</a></li>
  </ul>
</li>
Enter fullscreen mode Exit fullscreen mode

Using :has() to dynamically indicate parent pages

All links use an <li> tag, and all parent pages will have a nested <ul> tag containing the child links. This means we can specifically style all (and only) parent pages by targeting <li> tags that have a <ul> as a direct child using :has(). Again, place the selector within the parentheses; here I'm using the child combinator:

.links > li:has(> ul)::after {
  content: "";
  position: absolute;
  bottom: -20px;
  left: calc(50% - 10px);
  border-top: 10px solid #044b7f;
  border-bottom: 10px solid transparent;
  border-left: 10px solid transparent;
  border-right: 10px solid transparent;
  transition: border 0.4s;
}
Enter fullscreen mode Exit fullscreen mode

This selector says, "In the navigation list, find all <li> tags that have a direct <ul> child, and create the 'after' pseudo-element." All other <li> tags without this child element (non-parents) will not be affected. There is additional styling related to the hover and displaying of the submenu on hover, but the main selector that will continue to be used is the .links > li:has(> ul).

And that's all that's needed! In the HTML, if you were to create additional parent links in the navigation with child links, these would automatically get the same parent styling with the pseudo-element arrow without needing any additional markup or CSS classes.

Conclusion

These are just a couple of simple examples where :has() can make CSS more dynamic and convenient. There are ways to chain the selectors for much more complex uses, but hopefully this is enough to get you started with this powerful new functionality.

Top comments (0)