Skip to content

:where functional pseudo-selectors :is valuable in CSS

:where functional pseudo-selectors :is valuable in CSS

If you’ve written CSS before, you’ve used pseudo selectors.

Typically, we’d use them to style :hover events or select every :nth-of-type()

In recent years CSS has become a lot more powerful, and with that, we now have many more pseudo-selectors available to use.

Let’s explore some functional pseudo-selectors together and see how to use them to enhance our front-end code.

Functional pseudo-classes

While there are a wide range of pseudo-classes, I want to focus on the functional ones today.

:is

:is works very similar to a regular CSS class list at first glance

:is(a) {
 // this just provides styles to your <a> element
}

One of its main benefits is that you can group CSS classes to form more readable and maintainable conditions.

article :is(h1,h2,h3,h4,h5,h6) {
  font-weight: bold;
}

// replaces
article h1, article h2, article h3, article h4, article h5, article h6 {
  font-weight: bold;
}

For deep nesting, this can make your CSS significantly easier to understand, simplifying editing at a later date.

article .prose h1,
article .prose h2,
article .prose h3,
article .prose h4,
article .prose h5,
article .prose h6,
article aside h2,
article aside a,
article .call-to-action h2 {
  font-weight: bold;
}

// becomes
article :is(.prose, aside, .call-to-action) :is(h1, h2, h3, h4, h5, h6, a) {
  font-weight: bold;
}

You may be thinking that CSS has another solution that improves readability in a similar way: nesting. That’s true, and in this context, you can use them interchangeably, though the syntax is still shorter.

article :is(h1,h2,h3,h4,h5,h6) {
  font-weight: bold;
}

//is the same as
article {
  h1, h2, h3, h4, h5, h6 {
    font-weight: bold;
  }
}

However deeper nesting using nested CSS can become complex quickly. Sometimes it makes sense to use :is to help avoid this complexity.

article :is(.prose, aside, .call-to-action) {
  padding: 1rem;
}

article :is(.prose, aside, .call-to-action) :is(h1, h2, h3, h4, h5, h6, a) {
  font-weight: bold;
}

//With nested CSS
article {
  .prose, aside, .call-to-action {
    padding: 1rem;

    h1, h2, h3, h4, h5, h6, a {
      font-weight: bold;
    }
  }
}

:is also provides a specificity modifier you don’t get with nested css.

If you write:

article :is(h1, h2, h3, h4, h5, h6, .bold, #article-heading)

Every selector within the:is would be treated as if it had the same specificity value as the ID. With :is, the highest value applies to all other values within the :is. This does not apply to nested CSS.

:not

It does exactly what you think it does.

The :not selector grabs everything except the classes you define:

// Select all btn classes which are not links
.btn:not(a) {
  ...
}

Like the other pseudo-classes, you can negate multiple classes if you choose:

article :not(h1, h2, h3, h4, h5, h6) {
  font-family: inter;
}

This can be powerful when combined with other pseudo-selectors:

li:not(:last-child) {
  border-bottom: 1px solid #ccc;
}

:where

Using the :where pseudo-class is great for creating low-specificity CSS.

If you create a library or plugin that’s going to be used by other people and you want them to style it themselves, but don’t want it to appear ugly when they first set it up, :where is the way to do that.

For example, let’s style all our links with an underline using :where

// Select all links, with a specificity of 0
:where(a) {
  color: rebeccapurple;
  text-decoration: underline;
}

With this approach, we can override our link’s default styles without having to create difficult to maintain CSS:

a {
  text-decoration: none;
}

If we didn't use :where above, import order would matter if we wanted to override this. But because we planned ahead, we can just use a standard a tag.

Without using :where we’re stuck with options that are much harder to work with:

// We'd be forced to do something like this to increase specificity
* > a {
  text-decoration: none;
}

// Or add the ever-dreaded !important which we'd have to fight with later
a {
  text-decoration: none !important;
}

:where also has the same benefits of class grouping that :is does but without the specificity, so you can do something like this and then override it easily later.

article :where(h1, h2, h3, h4, h5, h6) {
  font-weight: bold;
}

:has

One of the most powerful pseudo selectors is :has, which gives you the option to style a tag or class based on other classes or tags associated with it.

// if active items have a different state, we can hide styles on previous elements
li:has(+ li.active) {
  border-bottom: none;
}

An incredible benefit :has brings is the ability to create parent selector functionality. This gives you a wide variety of options to style the parent based on the state of the children.

// The form would show an error state if any of the inputs are empty
form:has(input:empty) {
  border: var(--error);
}

// Remove padding for an article h1 only when it's followed by a subtitle
article h1:has(+ .subtitle) {
  padding-bottom: 0;
}

// add borders if a table cell has another table cell after it
table td:has(+ td) {
  border-right: 1px solid black;
}

You can also combine this with the :not selector to select elements that don’t have specific children.

// Add additional styles if the form button is hidden
form:not(:has(> button)) { ... }

Benefits of Pseudo-Classes

Readability

One of the benefits of all of these pseudo-selectors is that they can act similarly to nested CSS. It’s possible to use these to make your code more readable.

:is(a, button, .link) .small {
  font-size: 0.875rem;
}

// is the same as standard css
a .small, button .small, .link .small {
  font-size: 0.875rem;
}

// and is also the same as nested css, but smaller
a, button, .link {
  .small {
    font-size: 0.875rem;
  }
}

Using :is this way is effectively the same as using nested CSS, but if you need lower specificity you could use :where, or :not if you want to exclude (instead of include) some classes.

Functionality

:not and :has provide new options which weren’t possible before, allowing you to style more dynamically and provide better experiences while simplifying your code.

Before these options were available, the only solution was to style the code using JavaScript. While this technically allows you to achieve your styling goals, it’s not ideal. Mixing CSS operations into JS files makes it much harder to maintain long-term because it adds a layer of abstraction to your solution, while the built-in CSS option is much simpler.

While :is and :where don’t provide as much new functionality, they still allow you to write more understandable CSS with less ambiguity or workarounds, making maintenance significantly easier.

Summing up

Modern CSS allows us to be much more flexible with our styles, all while writing less code.

Simplifying CSS and removing the need to compensate, either by writing extra CSS or styling through JS, means our CSS files are more explicit and easier to maintain in the long term.

They may not be needed often, but they’re an incredibly important part of your front-end toolbelt.

Have you found any interesting ways to use functional pseudo-classes? Send us a post on X or message us on LinkedIn and show me what cool things you’ve made.

This Dot is a consultancy dedicated to guiding companies through their modernization and digital transformation journeys. Specializing in replatforming, modernizing, and launching new initiatives, we stand out by taking true ownership of your engineering projects.

We love helping teams with projects that have missed their deadlines or helping keep your strategic digital initiatives on course. Check out our case studies and our clients that trust us with their engineering.

Let's innovate together!

We're ready to be your trusted technical partners in your digital innovation journey.

Whether it's modernization or custom software solutions, our team of experts can guide you through best practices and how to build scalable, performant software that lasts.

Prefer email? hi@thisdot.co