Search
Close this search box.

Pseudo-class selectors are the ones that begin with the colon character “:” and match based on a state of the current element. The state may be relative to the document tree or in response to a state change such as “:hover” or “:checked”.

The Level 4 of CSS Selectors includes several pseudo-class selectors. This article will cover ones that currently have the best support, along with examples to demonstrate how we can start using them.

1- “:focus-within

The “:focus-within” pseudo-class has support among all modern browsers, and acts almost like a parent selector but only for a very specific condition.
When attached to a containing element and a child element matches for “:focus”, styles can be added to the containing element and/or any other elements within the container.

A practical enhancement to use this behavior for is styling a form label when the associated input has focus. For this to work, we wrap the label and input in a container, and then attach “:focus-within” to that container as well as selecting the label

.form:focus-within label {
  color: red;
}

This results in the label turning red when the input has focus.

2- “:focus-visible

The “:focus-visible” pseudo-class is intended to only show a focus ring when the user agent determines via heuristics that it should be visible. Put another way: browsers will determine when to apply “:focus-visible” based on things like (input method, type of element, and context of the interaction).
For testing purposes via a desktop computer with keyboard and mouse input, you should see “:focus-visible” styles attached when you tab into an interactive element but not when you click it, with the exception of (text inputs and textareas) which should show “:focus-visible” for all focus input types.

The latest versions of (Firefox and Chrome) browsers seem to now be handling “:focus-visible” on form inputs according to the Specifications which says that the user agent should remove “:focus” styles when “:focus-visible” matches. Safari is not yet supporting “:focus-visible” so we need to ensure a “:focus” style is included as a fallback to avoid removing the outline for accessibility.

In the following example:

input:focus,
button:focus {
outline: 1px solid red;
outline-offset: 0.5em;
}

input:focus-visible {
outline: 1px solid transparent;
border-color: red;
}

button:focus:not(:focus-visible) {
outline: none;
}

button:focus-visible {
outline: 1px solid transparent;
box-shadow: 0 0 0 1px #fff, 0 0 0 2px red;
}

The result in Chrome and Firefox

  • input” Correctly remove “:focus” styles when elements are focused via mouse input in favor of “:focus-visible” resulting in changing the “border-color” and hiding the outline on keyboard input.
  • button” Does not only use “:focus-visible” without the extra rule for “button:focus:not(:focus-visible)” that removes the outline on “:focus”, but will allow visibility of the box-shadow only on keyboard input.

The result in SAFARI

  • input” Continues using only the “:focus” styles.
  • button” This seems to now be partially respecting the intent of “:focus-visible” on the button by hiding the “:focus” styles on click, but still showing the “:focus” styles on keyboard interaction.

So for now, the recommendation would be to continue including “:focus” styles and then progressively enhance up to using “:focus-visible” which the demo code allows.

3- “:any-link

This pseudo-class has supported by most browsers. The any-link pseudo-class will match an anchor hyperlink as long as it has a “href”. It will match in a way equivalent to matching both “:link” and “:visited” at once. Essentially, this may reduce styles by one selector if adding basic properties such as color that apply to all links regardless of their visited status

:any-link {
color: greenyellow;
text-decoration: none;
}

An important note about specificity is that “:any-link” will win against “a” as a selector even if “a” is placed lower in the cascade since it has the specificity of “a” class. In the following example, the links will be red:

:any-link {
color: red;
}a {
color: green;
}

So if introduce “:any-link”, be aware that you will need to include it on instances of “a” as a selector if they will be in direct competition for specificity.

4- “:is()

Also known as the “matches any” pseudo-class, “:is()” can take a list of selectors to try to match against. For example, instead of listing heading styles individually, you can group them under the selector of “:is(h1, h2, h3)”.

Unique behaviors about the “:is()” selector list:

  • If a listed selector is invalid, the rule will continue to match the valid selectors. Given “:is(-uil-invalid, article, p)” the rule will match article and p.
  • The computed specificity will equal that of the passed selector with the highest specificity. For example, “:is(#id, p)” will have the specificity of the “#id” 1.0.0 whereas “:is(p, a)” will have a specificity of 0.0.1

The first behavior of ignoring invalid selectors is a key benefit. When using other selectors in a group where one selector is invalid, the browser will throw out the whole rule. This comes into play for a few instances where vendor prefixes are still necessary, and grouping prefixed and non-prefixed selectors causes the rule to fail among all browsers. With “:is()” you can safely group those styles and they will apply when they match and be ignored when they don’t.

Grouping heading styles as previously mentioned is already a big win with this selector. It’s also the type of rule that you will feel comfortable using without a fallback when applying non-critical styles, such as:

:is(h1, h2, h3) {
line-height: 1.5;
}

:is(h2, h3):not(:first-child) {
margin-top: 4em;
}

In this example having the greater “line-height” inherited from base styles or lacking the margin-top is not really a problem for non-supporting browsers. It’s simply less than ideal. What you wouldn’t want to use “:is()” for quite yet would be critical layout styles such as Grid or Flex that significantly control your interface.

Additionally, when chained to another selector, you can test whether the base selector matches a descendent selector within “:is()”. For example, the following rule selects only paragraphs that are direct descendants of articles. The universal selector is being used as a reference to the “p” base selector.

p:is(article > *)

For the best current support, if you’d like to start using it you’ll also want to double-up on styles by including duplicate rules using “:-webkit-any()” and “:matches()”.
Remember to make these individual rules, or even the supporting browser will throw it out! In other words, include all of the following:

:matches(h1, h2, h3) { }

:-webkit-any(h1, h2, h3) { }

:is(h1, h2, h3) { }

5- “:where()

The pseudo-class “:where()” is almost identical to “:is()” except for one critical difference: it will always have zero-specificity.
This has incredible implications for folks who are building frameworks, themes, and design systems. Using “:where()”, an author can set defaults and downstream developers can include overrides or extensions without specificity clashing.

Consider the following set of “div” styles. Using “:where()”, even with a higher specificity selector, the specificity remains zero.
In the following example

:where(article div:not(:first-child)) {
border: 1px solid red;
}

:where(article) div {
border: 1px solid green;
}

div {
border: 1px solid orange;
}

The first rule has zero specificity since its wholly contained within “:where()”. So directly against the second rule, the second rule wins. Introducing the div element-only selector as the last rule, it’s going to win due to the cascade. This is because it will compute to the same specificity as the “:where(article) div” rule since the “:where()” portion does not increase specificity.

Using “:where()” alongside fallbacks is a little more difficult due to the zero-specificity feature since that feature is likely why you would want to use it over “:is()”.
And if you add fallback rules, those are likely to beat “:where()” due to its very nature.

6- “:not()

The base “:not()” selector has been supported since Internet Explorer 9. But Selectors Level 4 enhances “:not()” by allowing it to take a selector list, just like “:is()” and “:where()”.

The following rules provide the same result in supporting browsers:

article :not(h2):not(h3):not(h4) {
margin-bottom: 1em;
}

article :not(h2, h3, h4) {
margin-bottom: 1em;
}

As with “:is()”, enhanced “:not()” can also contain a reference to the base selector as a descendent using “*”.

This example demonstrates this ability by selecting links that are not descendants of nav.

a:not(nav *) {
color: red;
}

Another example for “:not()”

img:not(:is(h2, h3) + *) {
border: 1px solid red;
}

In the example “:not()” and “:is()” are select the images that are not adjacent siblings of either “h2” or “h3” elements and give them border-color red.

As web designers and developers, we look forward to seeing all CSS4’s features supported on all popular browsers.

We use cookies to ensure that we give you the best experience on our website. If you continue to use this site we will assume that you are happy with it.