No Login Data Private Local Save

CSS Pseudo‑Class Playground - Online Test :is(), :where(), :not()

10
0
0
0

CSS Pseudo‑Class Playground

Interactive testbed for :is() :where() :not() — real‑time highlighting & specificity insights

HTML Structure editable
CSS Selector live evaluation
0 matched specificity info
Enter a selector to see matching results and specificity insights.
Live Preview
:is()

Matches any selector in its argument list. Takes the highest specificity among arguments.

:where()

Same as :is(), but always has zero specificity. Perfect for base styles you want to easily override.

:not()

Excludes elements matching its argument. Specificity equals the argument's specificity.

Frequently Asked Questions

:is() is a forgiving selector list pseudo-class. It matches any element that matches at least one selector in its argument list. Unlike traditional comma-separated selectors, if one selector inside :is() is invalid, the entire list is not discarded — the browser simply ignores the invalid selector and continues evaluating the rest.

Key use cases:

  • Shortening repetitive selectors: :is(header, main, footer) p instead of header p, main p, footer p
  • Grouping states: :is(:hover, :focus, :active)
  • Cross-browser styling with fallbacks

Specificity note: :is() adopts the specificity of its most specific argument. For example, :is(#id, .class) has the specificity of an ID selector (1,0,0).

The critical difference is specificity. While :is() takes the highest specificity from its arguments, :where() always contributes zero specificity — regardless of what selectors are inside it.

This makes :where() ideal for:

  • Base/reset styles that you want to be trivially overridable
  • Library/design-system defaults that consumers should easily override without specificity battles
  • Low-priority fallback rules

Example: :where(.dark-theme) a { color: lightblue; } has specificity (0,0,1) — just the a element. This means .dark-theme .nav a { color: white; } (specificity 0,2,1) easily overrides it.

:not() excludes elements that match its argument. It's extremely useful for:

  • Styling all but one: li:not(.active) { opacity: 0.6; }
  • Avoiding overrides: button:not([disabled]) { cursor: pointer; }
  • Layout exceptions: .card:not(:last-child) { margin-bottom: 16px; }
  • Combining with other pseudo-classes: :is(.card, .box):not(.featured)

Important: :not() takes only one simple selector in older browsers, but modern browsers support complex selector lists inside :not() (e.g., :not(.a, .b)), matching the forgiving behavior of :is().

Specificity: :not() contributes the specificity of its argument. :not(#id) has the specificity of an ID.

Yes, absolutely! These pseudo-classes can be nested and combined in powerful ways:

  • :is(.card, .box):not(.disabled) — match cards and boxes that aren't disabled
  • :where(header, main) :is(h1, h2, h3) — zero-specificity heading selection within header or main
  • :not(:is(.excluded, .hidden)) — exclude elements matching any of the listed classes
  • :is(:where(.a, .b), .c) — the :where() part contributes zero specificity, while .c contributes normally; overall specificity = max(0, class) = class-level

This composability is what makes these pseudo-classes so powerful for building maintainable, scalable CSS architectures.

All modern browsers fully support :is(), :where(), and :not() with complex selector lists:

  • Chrome 88+ (January 2021) — full support for forgiving selector lists in :is() and :where()
  • Firefox 78+ (June 2020)
  • Safari 14+ (September 2020)
  • Edge 88+ (January 2021)

Legacy support: Older browsers supported :not() only with simple selectors. The older :matches() (prefixed as :-webkit-any() / :-moz-any()) was the precursor to :is(). If you need to support very old browsers, provide fallback rules.

As of 2024, global support exceeds 96%, making these safe for production use in most projects.

Specificity is calculated as a three-part value (a, b, c):

  • a = number of ID selectors
  • b = number of class selectors, attribute selectors, and pseudo-classes
  • c = number of type (element) selectors and pseudo-elements

Rules for our pseudo-classes:

  • :is(A, B, C) → specificity = max(specificity(A), specificity(B), specificity(C))
  • :where(A, B, C) → specificity = (0, 0, 0) — always zero, regardless of arguments
  • :not(X) → specificity = specificity(X) — the negation itself adds nothing

Practical example: :is(#hero, .card) p → specificity of :is() part = max((1,0,0), (0,1,0)) = (1,0,0), plus p = (0,0,1) → total (1,0,1).

:where(#hero, .card) p:where() = (0,0,0), plus p = (0,0,1) → total (0,0,1).

Two main reasons: brevity and forgiving behavior.

Brevity: Compare header p, main p, footer p, aside p (72 chars) versus :is(header, main, footer, aside) p (37 chars). The longer the shared suffix, the more :is() saves.

Forgiving behavior: In a traditional comma-separated list like ::unknown, .valid, the entire rule is discarded if one selector is invalid. With :is(::unknown, .valid), the invalid ::unknown is simply ignored and .valid still matches. This is invaluable when using experimental or vendor-prefixed selectors.

When NOT to use: If the selectors share no common prefix/suffix, :is() adds unnecessary complexity. Use it when it genuinely simplifies your code.

For the vast majority of use cases, performance differences are negligible. Browsers have optimized selector matching to an extraordinary degree.

However, some nuances exist:

  • Overly broad :is(): A selector like :is(*) matches everything and can trigger unnecessary style recalculations. Be specific.
  • Deeply nested pseudo-classes: While supported, deeply nested structures like :is(:not(:where(:is(...)))) are harder for both humans and browsers to optimize. Keep it readable.
  • Large argument lists: A :is() with 50+ selectors is technically fine but likely indicates a design problem.

Rule of thumb: If your selector is readable and targets a reasonable number of elements, performance won't be an issue. Prioritize clarity and maintainability.