webdev.complete
💄 CSS — The Skin
🧱Foundations
Lesson 10 of 117
25 min

Selectors & the Cascade

How CSS decides which rule wins.

CSS is two things: a set of selectors (how do I pick the elements I want?) and a set of declarationsapplied to them. Selectors are where most CSS confusion lives. The moment you understand how the browser decides "which rule wins," CSS stops feeling magical.

The basic selectors

  • Type: matches an HTML element. p selects every paragraph.
  • Class: matches elements with a class. .card selects every element with class="card".
  • ID: matches one element by id. #header selects id="header". IDs should be unique per page.
  • Attribute: matches based on attributes. [type="email"] selects email inputs. [href^="https"] matches links starting with https.
  • Universal: * matches everything.
css
/* Type */
p { line-height: 1.6; }

/* Class */
.button { padding: 0.5rem 1rem; }

/* ID */
#site-header { background: #1d4ed8; }

/* Attribute */
input[type="email"] { font-family: monospace; }
a[href^="https"] { color: green; }
a[href$=".pdf"]::after { content: " (PDF)"; }

Combinators: relationships between selectors

  • Descendant: space. article p = any p inside an article, at any depth.
  • Child: >. ul > li = direct children only.
  • Adjacent sibling: +. h2 + p = the p immediately after an h2.
  • General sibling: ~. h2 ~ p = every p that comes after an h2 at the same level.

Pseudo-classes (state and structure)

Pseudo-classes start with one colon and describe a state or position of an element.

  • :hover - the cursor is over the element.
  • :focus - the element has keyboard or programmatic focus.
  • :focus-visible - focus, but only when shown by keyboard (not mouse). This is what you want for outlines.
  • :active - the element is being clicked right now.
  • :disabled, :checked, :required, :valid, :invalid - form states.
  • :first-child, :last-child, :nth-child(2n+1) - structural.
focus-visible is the right default
:focus shows the outline on mouse click too, which looks ugly. :focus-visible only triggers when the browser thinks the user is keyboard-navigating. Use it for visible focus indicators.

Pseudo-elements (generated content)

Pseudo-elements use two colons. They let you target parts of an element that aren't real elements in the HTML.

  • ::before / ::after - insert content before or after an element. Requires content.
  • ::placeholder - style placeholder text inside inputs.
  • ::selection - style highlighted/selected text.
  • ::marker - style list bullets/numbers.
css
/* Add a leading icon */
.external-link::after {
  content: " ↗";
}

/* Style the placeholder */
input::placeholder { color: #94a3b8; }

/* Custom selection color */
::selection { background: #1d4ed8; color: white; }

Specificity: the four-number tuple

When two rules target the same element, the browser computes a specificity score for each and the higher one wins. Specificity is a four-number tuple, read left to right:

  1. Inline styles (style="..."): 1, 0, 0, 0
  2. IDs: 0, 1, 0, 0 per ID
  3. Classes, attributes, pseudo-classes: 0, 0, 1, 0 each
  4. Type selectors and pseudo-elements: 0, 0, 0, 1 each
css
/* (0,0,0,1) */
p { color: black; }

/* (0,0,1,0) */
.lead { color: blue; }

/* (0,0,1,1) */
p.lead { color: red; }

/* (0,1,0,0) */
#highlight { color: green; }

The browser compares tuples component-by-component. (0,1,0,0) beats (0,0,9,0)because the ID column wins before classes even count. This is why "just add one more class" doesn't always work to override an ID.

Source order: the tiebreaker

If specificity is equal, the rule that appears later in the stylesheet wins. This is why CSS imports order matters: a theme imported after a reset overrides the reset.

!important: the last resort

Adding !important to a declaration trumps specificity. It is also the fastest way to make a stylesheet impossible to maintain. Limit it to:

  • Utility classes that should always win (some frameworks).
  • Fixing bugs in third-party CSS you can't edit.
  • Accessibility overrides that must win.
If you reach for !important, ask why first
Most !important usages are masking a specificity bug or a CSS architecture problem. Try refactoring before importing yourself into a corner.

The modern helpers: :is(), :where(), :has()

These three pseudo-classes changed CSS in the last few years.

  • :is(...)- "match any of these." Specificity is the most-specific argument. :is(h1, h2, h3) a targets links in any heading.
  • :where(...) - same as :is but with zero specificity. Perfect for resets and themeable defaults that should be easy to override.
  • :has(...) - the parent selector. Selects an element that contains something matching the inner selector. article:has(img) = articles that have images.
css
/* Match links in any heading */
:is(h1, h2, h3) a { color: inherit; }

/* Style a card differently if it has an image */
.card:has(img) { padding: 0; }
.card:has(img) h3 { margin-top: 1rem; }

/* Reset styles that don't fight you later */
:where(button, input, textarea) { font: inherit; }
:has() is huge
For 20 years, CSS could only style children based on parents. With :has()you can finally style a parent based on its children. "A form with an invalid field," "A list with more than 3 items," "A card containing an image" - all one selector now.

Try it: selector experiments

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="stylesheet" href="styles.css" />
  </head>
  <body>
    <h1>Selector lab</h1>

    <article class="card">
      <h2>With image</h2>
      <img src="https://placecats.com/300/120" alt="" />
      <p>This card has an image.</p>
    </article>

    <article class="card">
      <h2>Without image</h2>
      <p>This card does not.</p>
    </article>

    <form>
      <label>Email <input type="email" required /></label>
      <button>Submit</button>
    </form>

    <ul>
      <li>Apple</li>
      <li>Banana</li>
      <li>Cherry</li>
    </ul>
  </body>
</html>

Quick quiz

Quiz1 / 4

Which selector is more specific: '#main .card' or '.page .container .card.featured'?

Recap

  • Selectors: type, class, ID, attribute, universal.
  • Combinators: space (descendant), > (child), + (adjacent), ~ (sibling).
  • Pseudo-classes (one colon) describe state. :focus-visible is the modern focus default.
  • Pseudo-elements (two colons) target generated content. ::before/::after need content.
  • Specificity is a 4-number tuple: inline, IDs, classes/attr/pseudo, types. Tiebreaker: source order.
  • !important only when truly nothing else works.
  • :is(), :where(), :has() - modern selectors that make CSS expressive again.
Built with Next.js, Tailwind & Sandpack.
Learn. Build. Ship.