CSS Reference Guide

All TopicsPlayground

Selectors Deep Dive

Master advanced CSS selectors including combinators, pseudo-classes like :has() and :is(), attribute selectors, specificity rules, and native CSS nesting.

Selectors:has():is()SpecificityNestingCombinators

Combinators

Combinators define relationships between selectors. They let you target elements based on their position relative to other elements in the DOM tree.

CSS
/* Descendant (space) — any nested depth */
article p { color: #ccc; }
/* Matches: <article><div><p> (any depth) */

/* Child (>) — direct children only */
ul > li { list-style: none; }
/* Matches: <ul><li> but NOT <ul><ol><li> */

/* Adjacent sibling (+) — immediately next */
h2 + p { margin-top: 0; }
/* Matches the first <p> right after <h2> */

/* General sibling (~) — any following sibling */
h2 ~ p { font-size: 0.95rem; }
/* Matches ALL <p> siblings after the <h2> */
Live Example — Combinators in Action

Child (>) vs Descendant (space):

Direct child (matched by >)
Direct child (matched by >)
Nested descendant (only matched by space, not >)

Adjacent (+) vs General (~) sibling:

Heading (h2)
First para — matched by both + and ~ (green, bold)
Second para — matched only by ~ (italic)
Third para — matched only by ~ (italic)

Attribute Selectors

Target elements based on their HTML attributes and attribute values. Extremely powerful for styling links, form elements, and data-attribute driven UIs.

CSS
/* Has the attribute (any value) */
[title]       { cursor: help; }

/* Exact match */
[type="email"] { border-color: #667eea; }

/* Starts with (^=) */
[href^="https"] { color: #43e97b; }

/* Ends with ($=) */
[href$=".pdf"]  { color: #f5576c; }

/* Contains (*=) */
[class*="btn"]  { cursor: pointer; }

/* Case-insensitive flag (i) */
[href$=".PDF" i] { color: #f5576c; }

/* Whitespace-separated word match (~=) */
[class~="active"] { font-weight: bold; }

/* Starts with value or value- (|=) for lang attributes */
[lang|="en"] { quotes: "\201C" "\201D"; }
Live Example — Styled Links by Attribute
Secure External Link[href^="https"] → green
Download PDF Guide[href$=".pdf"] → red
GitHub Profile[href*="github"] → purple
Email Us[href^="mailto"] → blue

:is() — Matches-Any Pseudo-class

:is() takes a comma-separated list of selectors and matches if any of them match. It greatly reduces repetition. Its specificity equals the most specific selector in the list.

CSS
/* Without :is() — repetitive */
header a:hover,
nav a:hover,
footer a:hover {
  color: #667eea;
}

/* With :is() — clean! */
:is(header, nav, footer) a:hover {
  color: #667eea;
}

/* Works nested too */
article :is(h1, h2, h3) {
  font-weight: 700;
  line-height: 1.2;
}

/* Combining with other selectors */
.card :is(img, video, picture) {
  border-radius: 8px;
  width: 100%;
}
Live Example — :is() Reducing Repetition

Heading 3

This paragraph follows h3 and gets styled via adjacent sibling.

Heading 4

Same style applied to all heading levels using :is(h3, h4, h5).

Heading 5

One selector rule styles all three heading levels with gradient text.

:where() — Zero Specificity

:where() works exactly like :is() but its specificity is always zero. This makes it perfect for base styles that are easy to override.

CSS
/* :where() has zero specificity — easily overridden */
:where(article, section, aside) p {
  line-height: 1.7;
  color: #b0b0c0;
}

/* This single class easily overrides the :where rule */
.highlight {
  color: #f5576c;   /* wins because :where = 0 specificity */
}

/* Great for CSS resets */
:where(h1, h2, h3, h4, h5, h6) {
  margin: 0;
  font-size: inherit;
  font-weight: inherit;
}

/* vs :is() comparison */
:is(article, section) p { }   /* specificity: 0-0-2 (element + element) */
:where(article, section) p { } /* specificity: 0-0-1 (only p counts) */
Live Example — :where() vs :is() Specificity

This uses :where() styling — gray text (zero specificity).

This has a simple class override — green text wins easily because :where() = 0 specificity.

When to Use :is() vs :where()

Use :is() for your application styles where normal specificity is desired. Use :where() for utility/reset/library styles that consumers should easily override.

:has() — The Parent Selector

The long-awaited "parent selector." :has() selects an element if it contains a descendant matching the argument. It can also check for adjacent siblings, states, and more.

CSS
/* Card with image gets different layout */
.card:has(img) {
  grid-template-rows: auto 1fr;
}

/* Card without image */
.card:not(:has(img)) {
  padding: 2rem;
}

/* Form validation — label color based on input state */
label:has(+ input:invalid) {
  color: #f5576c;
}

label:has(+ input:valid) {
  color: #43e97b;
}

/* Style parent based on child hover */
.nav:has(a:hover) {
  background: rgba(102,126,234,0.1);
}

/* Page has a specific element */
body:has(.modal.open) {
  overflow: hidden;
}
Live Example — :has() Card With/Without Image
placeholder

Card With Image

:has(img) applies grid-template-rows for a structured layout.

Card Without Image

:not(:has(img)) gives this card extra padding and a gradient background instead.

Live Example — :has() Form Validation

Type in the fields — labels change color based on validity using :has(input:valid) and :has(input:invalid).

:not() — Negation Pseudo-class

Matches elements that do not match the given selector. The modern version accepts complex selector lists.

CSS
/* All paragraphs except .intro */
p:not(.intro) {
  font-size: 0.95rem;
}

/* All links except those with .no-style */
a:not(.no-style) {
  text-decoration: underline;
  color: #667eea;
}

/* All inputs except submit and reset */
input:not([type="submit"]):not([type="reset"]) {
  border: 1px solid #ccc;
  padding: 0.5rem;
}

/* Modern: comma-separated list in :not() */
:not(h1, h2, h3, h4, h5, h6) {
  /* matches everything except headings */
}

/* Last child without border */
li:not(:last-child) {
  border-bottom: 1px solid rgba(255,255,255,0.1);
}
Live Example — :not() Excluding Items
  • Active item (hoverable)
  • Another active item
  • Disabled item (no hover, line-through via :not)
  • Active item (no bottom border via :not(:last-child))

Compound Selectors

Combine multiple selectors without whitespace to require all conditions on the same element.

CSS
/* Element + class */
a.primary { color: #667eea; }

/* Element + attribute */
input[required] { border-left: 3px solid #f5576c; }

/* Multiple classes */
.btn.primary.large { font-size: 1.2rem; }

/* Class + pseudo-class */
.card:hover:not(.disabled) {
  transform: translateY(-4px);
}

/* Element + multiple pseudo-classes */
li:first-child:last-child {
  /* only child (when there's exactly one li) */
  border-radius: 8px;
}

Specificity Calculation

Specificity determines which CSS rule wins when multiple rules target the same element. It is calculated as a three-part value: (ID, CLASS, ELEMENT).

CSS
/*  Specificity:  (ID - CLASS - ELEMENT)  */

p                      /* 0-0-1 */
.card                  /* 0-1-0 */
#hero                  /* 1-0-0 */
p.intro                /* 0-1-1 */
#hero .title           /* 1-1-0 */
div#hero p.intro       /* 1-1-2 */
.card .title a:hover   /* 0-2-1  (:hover = class-level) */
[type="email"]         /* 0-1-0  (attributes = class-level) */
::before               /* 0-0-1  (pseudo-elements = element-level) */

/* :is() takes the highest specificity in its list */
:is(.card, #hero) p    /* 1-0-1  (#hero is highest) */

/* :where() is always zero */
:where(.card, #hero) p /* 0-0-1  (:where = 0 specificity) */

/* :not() and :has() use the specificity of their argument */
p:not(.intro)          /* 0-1-1  (.intro contributes class-level) */
.card:has(img)         /* 0-1-1  */
Live Example — Specificity Battle
SelectorSpecificityLevel
*0-0-0Universal
p0-0-1Element
.card0-1-0Class
[type="email"]0-1-0Attribute
.card .title0-2-0Two classes
#hero1-0-0ID
#hero .title p1-1-1ID + class + element
style=""InlineBeats all selectors

Selector Performance

Browsers read selectors right to left. While selector performance rarely matters in practice, understanding it helps you write better CSS.

CSS
/* SLOWER: browser finds ALL p, then checks ancestors */
body div.container ul li a p { }

/* FASTER: browser finds .card-title directly */
.card-title { }

/* Performance ranking (fastest to slowest):
   1. ID           #hero
   2. Class        .card
   3. Element      p
   4. Attribute    [type]
   5. Universal    *
   6. Combinators  .a .b > .c
*/
Don't Over-Optimize

Modern browsers are incredibly fast at matching selectors. A page with thousands of elements and hundreds of rules still resolves in milliseconds. Write readable, maintainable selectors first. Only optimize if profiling reveals a real bottleneck.

Native CSS Nesting (&)

CSS now supports nesting natively (no preprocessor needed). The & symbol represents the parent selector, similar to Sass/SCSS.

CSS
/* Native CSS Nesting */
.card {
  padding: 1.5rem;
  border-radius: 12px;

  /* Nested child selector */
  & .title {
    font-size: 1.2rem;
    font-weight: 700;
  }

  /* Nested pseudo-class */
  &:hover {
    transform: translateY(-4px);
  }

  /* Nested pseudo-element */
  &::after {
    content: "";
  }

  /* & at the end — parent becomes qualifier */
  .dark-theme & {
    background: #1a1a2e;
    /* Compiles to: .dark-theme .card */
  }

  /* Nested media query */
  @media (width < 768px) {
    padding: 1rem;
  }
}

/* Deeply nested */
.nav {
  & .menu {
    & .item {
      &:hover {
        color: #667eea;
        /* Result: .nav .menu .item:hover */
      }
    }
  }
}
Nesting Best Practice

Avoid nesting more than 3 levels deep. Deep nesting creates high-specificity selectors and makes CSS harder to maintain. Think of nesting as a convenience, not a requirement for every rule.

& is Optional for Element Selectors (with conditions)

For element type selectors, you must use &: write & p { } not p { } inside a nested context. For class, ID, and pseudo-selectors, & is implied: &:hover and &.active work as expected.

Gotchas

:is() Forgives, :has() Can Be Slow

:is() and :where() are forgiving selector lists — if one selector is invalid, the rest still work. However, :has() involving complex selectors can be performance-intensive on large DOMs. Avoid *:has() on the universal selector.

:is() Takes Highest Specificity

:is(.card, #hero) takes the specificity of #hero (1-0-0) for the entire selector. If you just wanted class-level specificity, use :where() instead or separate the selectors.

:has() Cannot Be Nested

You cannot nest :has() inside another :has(). .a:has(.b:has(.c)) is invalid. You can, however, use :has() with other pseudo-classes: .card:has(img):not(.featured) works fine.

Nesting Specificity Adds Up

Native CSS nesting produces the same selectors as writing them flat. .a { & .b { & .c { } } } creates .a .b .c with specificity 0-3-0. Deep nesting means high specificity, making overrides difficult.

Pro Tips

:has() for State-Driven Styling

Use :has() to style parent elements based on child state without JavaScript: form:has(:invalid) .submit { opacity: 0.5; } disables the submit button appearance when any field is invalid.

Attribute Selectors for Icon Systems

Use [class^="icon-"] or [class*=" icon-"] to apply base icon styles to any element with an icon class, regardless of which specific icon it is.

:where() for Library CSS

If you are building a component library, wrap your selectors in :where() so consumers can override styles with a single class. This is what modern CSS resets (like Josh Comeau's) use.

Specificity Escape Hatch

If you need to override high-specificity rules without !important, you can stack :is() or repeat a class: .card.card has specificity 0-2-0 and beats 0-1-0 without resorting to IDs or inline styles.

Combine :has() with Sibling Combinators

:has() is not limited to descendants. .input:has(+ .error) selects an input that has an adjacent sibling with class .error. This lets you style elements based on what comes after them.

Browser Support

Modern selector support across major browsers (as of 2025).

Browser Compatibility Table
FeatureChromeFirefoxSafariEdge
:is()88+78+14+88+
:where()88+78+14+88+
:has()105+121+15.4+105+
:not() (multi-arg)88+84+9+88+
CSS Nesting (&)120+117+17.2+120+
[attr i] (case-insensitive)49+47+9+79+
Attribute selectors1+1+3+12+
Combinators (>, +, ~)1+1+1+12+
Previous CSS Functions