New Pseudo-Class Selectors Worth Using Now

Selectors Level 4 introduces several pseudo-class selectors that are gaining solid browser support. These selectors match elements based on their state—relative to the document tree or in response to interaction—and many are ready to use in production today.

The :any-link pseudo-class matches any anchor with an href, effectively combining :link and :visited into one selector. It has enjoyed cross-browser support for some time and can simplify your stylesheet when you want to apply basic properties like color to all links regardless of visit state.

:any-link {
  color: blue;
  text-underline-offset: 0.05em;
}

Watch out for specificity, though: :any-link carries the specificity of a class, so it will override a plain a selector even if that rule appears later in the cascade. In the example below, links render purple:

:any-link {
  color: purple;
}

a {
  color: red;
}

If you adopt :any-link, update existing a selectors that might compete with it to avoid unexpected styling conflicts.

:focus-visible for Smarter Focus Indicators

Removing outline on :focus is one of the most common accessibility mistakes on the web. That outline is essential for keyboard users navigating an interface, and its visibility is required by WCAG Success Criterion 2.4.11 (Focus Appearance Minimum).

The :focus-visible pseudo-class lets the browser decide when a focus ring is needed based on input method, element type, and interaction context. On a desktop with both mouse and keyboard, you will typically see :focus-visible styles when tabbing into an element but not when clicking it—except for text inputs and textareas, which should always show the indicator.

Chromium and Firefox now handle :focus-visible on form inputs per the spec, removing :focus styles when :focus-visible matches. Safari does not yet support :focus-visible, so always include a :focus rule as a fallback to preserve the outline.

input:focus,
button:focus {
  outline: 2px solid blue;
  outline-offset: 0.25em;
}

input:focus-visible {
  outline: 2px solid transparent;
  border-color: blue;
}

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

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

Testing the above styles yields the following:

Chromium and Firefox

  • input: Correctly drops :focus styles for mouse focus in favor of :focus-visible, changing border-color and hiding the outline on keyboard input.
  • button: Needs an explicit button:focus:not(:focus-visible) rule to remove the outline on mouse click; the box-shadow appears only on keyboard focus.

Safari

  • input: Continues applying only the :focus styles.
  • button: Partially respects :focus-visible intent—hides :focus styles on click but shows them for keyboard interaction.

The safest approach is to keep :focus styles and progressively enhance with :focus-visible, as the demo above illustrates.

:focus-within for Ancestor Styling

All modern browsers support :focus-within, which applies styles to a containing element when any descendant receives focus. It behaves almost like a parent selector but only for this specific condition.

A practical use case is highlighting a form label when its associated input is focused. Wrap the label and input in a container, then target the label via the container's :focus-within state:

.form-group:focus-within label {
  color: blue;
}

This turns the label blue when the input gains focus. You can also use the same mechanism to add an outline directly to the container.

:is() for Safer Selector Groups

:is(), the “matches any” pseudo-class, accepts a selector list and matches if any selector applies. For example, :is(h1, h2, h3) replaces three separate heading rules.

Two behaviors distinguish :is():

  • Invalid selectors in the list are ignored; the rule still matches the valid ones. Given :is(-ua-invalid, article, p), the rule matches article and p.
  • Specificity equals the highest-specificity selector in the list. :is(#id, p) has the specificity of #id (1.0.0), while :is(p, a) is 0.0.1.

Ignoring invalid selectors is a major benefit. With a standard grouped selector, one invalid entry invalidates the entire rule—a real problem when mixing prefixed selectors. :is() lets you group such styles safely; they apply where supported and are ignored elsewhere.

Grouping heading styles is a compelling use case, and it is safe to apply non-critical properties without a fallback:

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

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

This example is taken from the document styles in SmolCSS. Missing a margin-top or inheriting a larger line-height in non-supporting browsers is at worst less than ideal. However, avoid using :is() for critical layout styles, such as Grid or Flex, until support is more universal.

You can also reference the base selector as a descendant within :is() using the universal selector. The following rule targets paragraphs that are direct children of articles:

p:is(article > *)

For wider support, duplicate your rules with :-webkit-any() and :matches() as separate declarations—combining them into one grouped rule would cause all browsers to drop it:

:matches(h1, h2, h3) { }

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

:is(h1, h2, h3) { }

The related @supports selector at-rule (and its not variant) accompanies these selectors, but Safari does not yet support it. That matters because trying to feature-detect :is() via @supports selector would inadvertently exclude Safari, which does support the selector itself:

@supports selector(:is(h1)) {
  :is(h1, h2, h3) {
    line-height: 1.1;
  }
}

:where() for Zero Specificity

:where() behaves almost identically to :is(), with one crucial difference: its specificity is always zero. That makes it an excellent tool for theme and design-system authors who want to set defaults without creating specificity battles for downstream developers.

Consider these img rules. Even though the first selector contains more elements, :where() keeps its specificity at zero:

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

:where(article) img {
  border: 5px solid green;
}

img {
  border: 5px solid orange;
}

Since the first rule has zero specificity, the second rule wins in a direct comparison. Adding an element-only img rule last also wins, because it computes to the same specificity as :where(article) img with the :where() portion contributing nothing.

Crafting fallbacks for :where() is problematic: the zero-specificity that makes it attractive means any fallback rule would almost certainly override it. Add in @supports selector gaps, and feature detection becomes impractical. The better approach is to check your own analytics to confirm whether beginning to use :where() is safe for your audience.

Enhanced :not()

The original :not() dates back to Internet Explorer 9, but Selectors Level 4 lets it accept a full selector list like :is() and :where(). The following two rules produce identical results in supporting browsers:

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

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

Selector-list support for :not() is strong across all modern browsers. As with :is(), you can include a descendant reference to the base selector using *—for example, selecting links that are not within a nav. Chaining is also possible, such as targeting images that are not adjacent siblings of h2 or h3 elements.

The :has() Proposal and Its Uncertain Future

Among the most anticipated selector proposals is :has(), currently listed as “at-risk” in the Selectors Level 4 Editor’s Draft. That designation signals real challenges in completing a full implementation, and it may ultimately be dropped from the final recommendation. No browser currently supports it, even experimentally.

If it does make it through, :has() would function as the long-desired “parent selector.” Its logic combines elements of :focus-within and :is() with descendant combinators: you target an element based on the presence of certain descendants, but the styles apply to the parent itself. For example, the rule below would reduce the vertical padding of a navigation only when it contains a button:

nav {
  padding: 0.75rem 0.25rem;

nav:has(button) {
  padding-top: 0.25rem;
  padding-bottom: 0.25rem;
}

While no browser implements this today, the selector has generated considerable discussion about what becomes possible when styling can react to an element’s children.

A Useful Level 3 Selector: :empty

One pseudo-class from Selectors Level 3 that often flies under the radar is :empty. It matches an element with no child elements at all—including no text nodes. So p:empty would select <p></p> but not <p>Hello</p>.

A common practical use is hiding placeholder containers that await dynamic content from JavaScript. Suppose a div is meant to display search results. Until results arrive, it exists only as an empty box; you likely don’t want it occupying layout space with borders and padding. With :empty, you can hide it entirely:

.search-results:empty {
  display: none;
}

Be cautious, though, about using pseudo-elements and content to populate an empty state with a message. Assistive technology is inconsistent in exposing that generated content. For an accessible “no results” notice, add a real element—such as a paragraph—to the DOM. An aria-label on the hidden container would not reach assistive tech users reliably.

Where to Go Deeper

Selectors—pseudo-classes included—are a broad topic with plenty of resources to explore: