Replacing React code with CSS :has selector

Nadia Makarevich - Sep 16 - - Dev Community

Image description

Since the dawn of time… okay, since the beginning of CSS at least, we have been taught that CSS is cascading. It's literally in the name, they are Cascading Style Sheets. Via CSS, an element can target an element inside of it, then an element inside, etc. But never, ever in the reverse order. An element can not apply styles to a parent element by any means other than via JavaScript.

Until now.

The CSS :has selector is now supported by all major browsers, and with it, we actually can now target parent elements. And more! The world, indeed, is turned upside down. If you, like me, started your dev career in those blessed times when we were doing round corners on elements via transparent GIFs, the possibilities today will blow your mind.

So, other than being a cool new toy, what practical use does it actually have in the React world? Let's take a look at three very exciting ones.

What is the :has selector?

If you remember, in "standard" CSS, we can do this:

.content .card {
  background: #f0f0f0;
}

.content img {
  margin: 1rem 0;
}
Enter fullscreen mode Exit fullscreen mode

This changes the background of a .card element inside a .content element to light grey and adds margins to images inside. So that it's visually separated from the text.

We can also select the next sibling with + or ~ combinators. For example, if this image is right after the .card element, we might want to add an additional margin to it so that it's even more visually separated.

// images that immediately follow a card element
// will have bigger margins than other images
.content .card + img {
  margin: 2rem 0;
}
Enter fullscreen mode Exit fullscreen mode

Check out the code example here.

However, until recently, we couldn't select elements in the "opposite" direction. If I wanted to change the background of a .card element that is immediately followed by an image, for example, that would've been impossible without JavaScript. Or to style the .card differently if it had an image inside - also nope.

The new CSS selector :has fixes that.

I want to have pink borders on my .card elements with images inside and grey borders on all the others? Easy peasy!

// all the cards will have grey top borders
.card {
  border-top: 10px solid #f6f7f6;
}

// cards with images inside will have pink borders
.card:has(img) {
  border-top: 10px solid #fee6ec;
}
Enter fullscreen mode Exit fullscreen mode

The :has selector works with other selectors too. I also want to add blue borders on cards that are followed by an image? Sure thing! We can check that condition with the + combinator.

// if a card has an image as a next element - give it a blue border
.card:has(+ img) {
  border-top: 10px solid #c4f4ff;
}
Enter fullscreen mode Exit fullscreen mode

We can even go crazy and code something like "apply a green background to a .card element that does not have an h3 tag inside, has an img tag inside, has another .card element immediately after, and an img tag anywhere after that, but only if it's followed by a card with more than one image".

// have fun reading that ;)
.card:not(:has(h3)):has(img):has(+ .card):has(~ img):has(~ .card):has(> img:nth-child(1)) {
  background-color: #c3dcd0;
}
Enter fullscreen mode Exit fullscreen mode

Check out the implemented example below.

However, when it comes to our React apps, is this really something we want to do? Styles like that are heavily leaking through component boundaries, the same as child selectors. Didn't we spend the last decade or so inventing creative ways to prevent exactly that? BEM, SASS, CSS-in-JS, CSS modules… We do everything in our power to scope the styling only to the elements it's assigned to.

Why would we suddenly revert everything and do the opposite of all the best practices? Other than the fact that we tend to do that every few years in React anyway, of course 😅

The answer: so that we can remove a bunch of complicated React code! Sometimes, the best React code is no React code. And despite the last crazy selector (seriously, don't do that to your colleagues), replacing React with CSS can simplify the logic and even sometimes improve performance a bit.

Let's take a look at some practical examples of that.

:has selector and focus state on elements

Imagine we're implementing a task board. The board will have a bunch of cards, each card has two buttons: "open" and "delete". Clicking on the "open" button will open the full content of the card in a modal. Clicking on "delete" deletes the card.

Image description

The card's code here is trivial:

<div className="card">
  Some text here
  <div className="buttons">
    <button>
      <Open />
    </button>
    <button>
      <Delete />
    </button>
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode

We want it to be fully keyboard accessible: tabbing to those buttons should work. On top of that, I want the cards to highlight which button is tabbed into to improve accessibility for keyboard users. When a user tabs to any of those buttons, I want the card to "pop" slightly and the other cards in the column to grey out. Plus, I want to change the border color of the active card to highlight the current active interaction. Red for the "delete" button, and green for the "open" button.

Image description

If we were to implement this functionality with React, we'd have to add a focus event listener, detect which button is currently active, maintain the state so that we can change the classNames on the card itself, and somehow share that state with the parent so that other cards can be changed as well. We would probably have to introduce Context or some other state management solution for it. Before you know it, we'd have to implement a full-blown focus manager that re-renders every card on every tab. No wonder we don't see this fancy interactivity in the real world, and the best we can hope for is consistent outlines on buttons.

With the :has selector, however, it's more or less trivial to implement what I just described.

First step. Assign some data- attributes to the buttons so that we can select them without relying on className:

// add data-action attributes to the buttons
<button data-action="open">
  <Open />
</button>
<button data-action="delete">
  <Delete />
</button>
Enter fullscreen mode Exit fullscreen mode

Second step. Find the card with the focused "delete" button inside and change its CSS:

// make the card "pop" and change its border colors
// if the "delete" button inside the card is focused
.card:has([data-action='delete']:focus-visible) {
  border-top: 10px solid #f7bccb;
  box-shadow: 0 0 0 2px #f7bccb;
  transform: scale(1.02);
}
Enter fullscreen mode Exit fullscreen mode

Third step. Find the card with the focused "open" button inside and change its CSS:

// make the card "pop" and change its border colors
// if the "open" button inside the card is focused
.card:has([data-action='open']:focus-visible) {
  transform: scale(1.02);
  border-top: 10px solid #c3dccf;
  box-shadow: 0 0 0 2px #c3dccf;
}
Enter fullscreen mode Exit fullscreen mode

Forth step. This is the hardest one: we need to find all the cards before and after the card with focused open or delete buttons and then grey them out. The magic:

// all cards after the card with focused "open" button
.card:has([data-action='open']:focus-visible) ~ .card,

// all cards after the card with focused "delete" button
.card:has([data-action='delete']:focus-visible) ~ .card,

// all cards before the card with focused "open" button
.card:has(~ .card [data-action='open']:focus-visible),

// all cards before the card with focused "delete" button
.card:has(~ .card [data-action='delete']:focus-visible) {
  filter: greyscale();
  background-color: #f6f7f6;
}
Enter fullscreen mode Exit fullscreen mode

The end result: the most beautiful keyboard navigation in all the boards ever with zero JavaScript and zero React re-renders! A live example is below to play around with.

:has selector and categories of stuff

Another use case for the :has selector that I find fascinating in its simplicity is color-coding stuff based on some data.

For example, let's implement a table with products that are sold in our shop. These products fit into specific categories: let's say we're selling office supplies, clothes, and horses online. The table will have a few columns and, in its simplest form, will look something like this:

Image description

and be coded like this:

...
<tr>
  <td>Socks</td>
  <td>Created by...</td>
  <td>Inventory full</td>
  <td>
    <span className="category">
      clothes
    </span>
  </td>
</tr>
...
Enter fullscreen mode Exit fullscreen mode

Now, I want to subtly highlight which row belongs to which category by drawing a border on the left with the category's color. And when an inventory is empty, I want to highlight that row with a red background so that people pay attention to it. This is how I want it to look:

Image description

In React, we'd have to pass information about the category and the inventory through props to at least the row tag, maybe even the first cell. And create class names or even internal components for every variation. Totally unnecessary complication in this case.

Instead, we can just do this.

Step 1. Add data- attributes with the information to the cells that already have that information.

<tr>
  <td>Socks</td>
  <td>Created by...</td>
  <!-- add data-inventory attribute here -->
  <td data-inventory="full">Inventory full</td>
  <td>
    <!-- add data-category attribute here -->
    <span className="category" data-category="clothes">
      clothes
    </span>
  </td>
</tr>
Enter fullscreen mode Exit fullscreen mode

Step 2. Color-code everything we need with the help of the :has attribute.

Add different colored borders to the first cell of the row if the row has an element with the data-category attribute.

.table tr:has([data-category='clothes']) td:first-child {
  border-left: 6px solid #f7bccb;
}

.table tr:has([data-category='office']) td:first-child {
  border-left: 6px solid #f4d592;
}

.table tr:has([data-category='animals']) td:first-child {
  border-left: 6px solid #c4f4ff;
}
Enter fullscreen mode Exit fullscreen mode

Add the red background if the row has an element with the data-inventory attribute with the empty value.

.table tr:has([data-inventory='empty']) {
  background: #f6d0ce;
}
Enter fullscreen mode Exit fullscreen mode

And voila - the table is beautifully color-coded. The coolest part here is that if those attributes come from a dynamic state and tend to be updated frequently, the entire row won't have to re-render to update the colors! Only the cell with the data-attribute. Again, a tiny potential performance improvement in addition to the cleaner code.

Check out the interactive example below.

:has selector and form elements

And finally, a very powerful use case for the:has selector that I really like is styling elements based on the form elements' state.

For example, in a form where inputs can be disabled, we can also visually "disable" the input's label and description.

Image description

The code for this form will look something like this:

 <form className="form">
  <fieldset>
    <label htmlFor="form-name">Name</label>
    <input type="text" name="name" id="form-name" disabled value="Nadia" />
    <div className="description">Just your first name is fine</div>
  </fieldset>

  <fieldset>
    <label htmlFor="form-email">Email Address</label>
    <input type="email" name="email" id="form-email" required />
    <div className="description">We don't accept gmail domains!</div>
  </fieldset>
</form>
Enter fullscreen mode Exit fullscreen mode

Then in CSS, we'd target a fieldset that has an input with the :disabled state, and style label and .description elements.

fieldset:has(input:disabled) label,
fieldset:has(input:disabled) .description {
  color: #d6d6d6;
}
Enter fullscreen mode Exit fullscreen mode

Focus, of course, will also work. If we want to add a line at the left when the input is focused,

Image description

we can just do this:

fieldset:has(input:focus-visible) {
  border-left: 10px solid #c4f4ff;
}
Enter fullscreen mode Exit fullscreen mode

Or, if we're implementing a list with checkboxes, we can easily highlight the "checked" row without even storing the checkbox's state and creating an .active class, as we usually do for situations like this.

Image description

All we need is a CSS selector:

.list-with-checkboxes li:has(input:checked) {
  background: rgba(196, 244, 255, 0.3);
}
Enter fullscreen mode Exit fullscreen mode

Here's the live preview, check it out:


How cool is all of this, right? What's your favorite :has-related trick? Share in the comments!

Of course, there are many more good opportunities to simplify our React code with clever selectors. These are just a few examples that I particularly liked. If you want to learn more about the :has selector and play around with more cool examples, here's the list of articles I particularly liked that have plenty of those:

By looking at how CSS has progressed in recent years, maybe in five-ish years, we won't need React at all. 🤯 What an interesting day that would be!


Originally published at https://www.developerway.com. The website has more articles like this 😉

Take a look at the Advanced React book to take your React knowledge to the next level.

Subscribe to the newsletter, connect on LinkedIn or follow on Twitter to get notified as soon as the next article comes out.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .