CSS

:where functional pseudo-selectors :is valuable in CSS

Dan Spratling
4 min read
Dan - CSS's :has and :is psuedo selectors
This article was written over 18 months ago and may contain information that is out of date. Some content may still be relevant, but please refer to official documentation for the latest information.

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.

About the author

Keep reading

View all posts →

CSS Container Queries, what are they?

In this blog post, we take a look at container queries, a new feature that makes designing websites easier and more flexible. ...

Jesús Padrón6 mins
CSS

An example-based guide to CSS Cascade Layers

CSS Cascade Layers make style management simple—see how in this example-driven guide....

Dane Grant4 mins
CSS

Understanding the Difference Between `:focus` and `:focus-visible` in CSS

Understanding the Difference Between :focus and :focus visible in CSS I have learned my fair share about the importance of keyboard accessibility, so I know that visual indication of the focused element is very important. But the well known :focus pseudo class is not always the best fit for this job. That's where :focus visible comes in. Let's look at the differences between these two pseudo classes and explore the best practices for using them effectively. What is the :focus Pseudo Class? The :focus pseudo class is a CSS selector that applies styles to any element that receives focus, regardless of how that focus was triggered. This includes focus events from keyboard navigation, mouse clicks, and touch interactions. Example Usage of :focus In this example, the button will display a blue outline whenever it is focused, whether the user clicks on it with a mouse, taps it on a touchscreen, or navigates to it using the keyboard. What is the :focus visible Pseudo Class? The :focus visible pseudo class is more specialized. It only applies styles to an element when the browser determines that the focus should be visible. This typically occurs when the user navigates via the keyboard or assistive technologies rather than through mouse or touch input. Example Usage of :focus visible Here, the button will only show a blue outline when focused through keyboard navigation or another input method that usually requires visible focus indicators. Key Differences Between :focus and :focus visible :focus Behavior: Applies to any element that receives focus, regardless of the input method. Use Cases: Ensures that all interactions with the element are visually indicated, whether by mouse, keyboard, or touch. :focus visible Behavior: Applies styles only when the focus should be visible, such as using a keyboard or assistive technology. Use Cases: Ideal for scenarios where you want to provide focus indicators only to keyboard and assistive technology users while avoiding unnecessary outlines for mouse and touch users, typically required by design. Accessibility Implications :focus Pros: Guarantees that all users can see when an element is focused, which is critical for accessibility. Cons: Can lead to a suboptimal experience for mouse users, as focus styles may appear unnecessarily during mouse interactions. :focus visible Pros: Enhances user experience by showing focus indicators only when necessary, thus keeping the interface clean for mouse and touch users. Tailors the experience for keyboard and assistive technology users, providing them with clear visual cues. Cons: Additional considerations may be required to ensure that focus indicators are not accidentally omitted, especially in older browsers that do not support :focus visible. There may be cases where you want to show focus indicators for all users, regardless of input method. Best Practices for Using :focus and :focus visible To achieve the best accessibility and user experience, combining both :focus and :focus visible in your CSS is often a good idea. Combining :focus and :focus visible Here is a Stackblitz example of what such styling could look like for you to try out and play with. Additional Tips Test with Keyboard and Assistive Technology: Ensure that your web application is navigable using a keyboard (Tab, Shift + Tab, etc.) and that focus indicators are visible for those who rely on them. It's never a bad idea to include accessibility testing in your e2e testing suite. Provide Clear Focus Indicators: Make sure that focus indicators are prominent and easy to see. A subtle or hard to spot focus indicator can severely impact accessibility for users who rely on keyboard navigation. Conclusion The :focus visible pseudo class offers a more refined way to manage focus indicators, improving accessibility and user experience, particularly for keyboard and assistive technology users. By understanding the differences between :focus and :focus visible, and applying best practices in your CSS, you can create more accessible and user friendly web applications. Remember, accessibility should never be an afterthought. By thoughtfully applying focus styles, you ensure that all users, regardless of how they interact with your site, can easily navigate and interact....

Jan Kaiser3 mins
AccessibilityCSSPlaywright

How to Truncate Strings Easily with CSS

Learn how to truncate text in CSS, focusing on single-line and multi-line truncation using properties like overflow, text-overflow, and -webkit-line-clamp. It highlights CSS’s simplicity and responsiveness compared to JavaScript-based truncation....

Mark Shenouda4 mins
CSS