C.W.K.
Stream
Lesson 01 of 05 · published

The Selector Zoo: Every Pattern You'll Need

~13 min · selectors, pseudo-classes, combinators, attribute-selectors

Level 0Markup Novice
0 XP0/34 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"A selector is a question CSS asks the DOM. 'Show me every element that matches this shape.' The answer is the set the rule applies to."

The Five Categories

Every CSS selector falls into one (or a combination) of five categories:

  1. Simple selectors — match by element type, class, ID, attribute, or everything.
  2. Pseudo-classes — match by state (:hover, :checked) or position (:nth-child).
  3. Pseudo-elements — match conceptual parts of an element (::before, ::placeholder).
  4. Combinators — describe relationships between elements (space, >, +, ~).
  5. Logical combinators — group selectors with logic (:is(), :where(), :not(), :has()).

Simple Selectors

  • h1 — every <h1> element. (type selector)
  • .btn — every element with class btn. (class selector)
  • #main — the element with id main. (id selector — IDs must be unique per page)
  • * — every element. (universal selector — useful in resets, expensive in selector matching)
  • [type="email"] — every element with the matching attribute. (attribute selector)

Attribute Selectors: A Mini Query Language

Attribute selectors are more powerful than they look:

  • [disabled] — has the attribute (any value).
  • [type="email"] — exact match.
  • [class~="primary"] — class list contains the whole word.
  • [lang|="en"] — value is exactly en or starts with en-.
  • [href^="https"] — starts with.
  • [href$=".pdf"] — ends with.
  • [href*="github.com"] — contains.
  • [data-state="open" i] — the i flag makes it case-insensitive.

This is how you style external links differently from internal ones, or add an icon to every PDF link, with no class names at all.

Pseudo-Classes: State And Position

State pseudo-classes match the element's current state:

  • User actions: :hover, :focus, :focus-visible, :focus-within, :active
  • Form state: :checked, :disabled, :required, :optional, :valid, :invalid, :placeholder-shown
  • Link state: :link, :visited
  • Content state: :empty (no children), :target (the URL fragment)
  • Negation: :not(selector) — match elements that don't match the inner selector

Position pseudo-classes match by location among siblings:

  • :first-child, :last-child, :only-child
  • :nth-child(n), :nth-child(odd), :nth-child(2n+1), :nth-child(3n)
  • :first-of-type, :nth-of-type(n) — like the above but only counting siblings of the same element type

Pseudo-Elements: Style Parts That Aren't Real Elements

Pseudo-elements style conceptual parts of an element. Notice the double colon:

  • ::before, ::after — generated content. Always paired with content: "...". Used for icons, decorative quotes, list markers, badges.
  • ::placeholder — input placeholder text styling.
  • ::selection — the highlighted text when the user selects.
  • ::first-letter, ::first-line — typographic effects.
  • ::marker — the bullet/number on list items.
  • ::backdrop — the dimmed overlay behind <dialog> when open via showModal().

Combinators: Element Relationships

  • A B — descendant. B anywhere inside A. article p = every <p> inside any <article>.
  • A > B — direct child. ul > li = only the top-level <li>s, not nested ones.
  • A + B — adjacent sibling. h2 + p = the <p> immediately following an <h2>.
  • A ~ B — general sibling. h2 ~ p = every <p> after the <h2> at the same level.

Logical Selectors: The Modern Power Tools

  • :is(a, b, c) — matches if any inner selector matches. :is(h1, h2, h3) a = anchors inside h1, h2, or h3. Specificity = highest of the arguments.
  • :where(a, b, c) — same matching, but specificity always 0,0,0,0. Magic for design system base styles.
  • :not(a, b) — matches if no inner selector matches. li:not(:last-child) = every list item except the last.
  • :has(selector) — the parent selector. article:has(img) = articles that contain an image. form:has(input:invalid) = forms with invalid inputs.

Selector Lists: Comma-Separated

h1, h2, h3 { font-family: sans-serif; } — apply the same rule to every selector in the list. Each is evaluated independently, then specificity is computed per match. (Note: in :is() the list is treated as one logical group; that's the difference.)

Right-to-left matching is how the browser does it. CSS selectors are evaluated right-to-left (the rightmost simple selector is matched first, then walked up). This is why * { ... } is the slowest selector (every element matches; then each one is checked for its ancestors). Performance only matters at massive scale — for normal sites, write the clearest selector and let the browser optimize.

Pippa's Note

Cwk-site's Tailwind classes resolve down to selectors like .flex, .gap-4, .text-amber-500 — every one is a simple class selector. Tailwind avoided the cascade wars by giving every utility a (0,0,1,0) specificity and relying on source order. Knowing the selector zoo means you understand why Tailwind chose that strategy, not just that it works.

Code

Simple + attribute selectors·css
/* Simple selectors */
h1 { font-size: 2.5rem; }              /* type */
.btn { padding: 0.5rem 1rem; }          /* class */
#main { padding: 2rem; }                /* id (unique) */
* { box-sizing: border-box; }           /* universal */
[type="email"] { font-family: monospace; }  /* attribute */

/* Attribute selectors as a query language */
a[href^="https"] { color: #5BA3D8; }   /* external links */
a[href$=".pdf"]::after { content: " 📄"; }  /* PDF links */
a[href*="github.com"] { background-image: url(/github-icon.svg); }
input[type="checkbox"]:checked { accent-color: #5BA3D8; }
Pseudo-classes + pseudo-elements·css
/* Pseudo-classes: state */
button:hover { background: #5BA3D8; }
button:focus-visible { outline: 2px solid #5BA3D8; }
input:invalid { border-color: #d93b3b; }
form:has(input:invalid) button[type="submit"] { opacity: 0.6; }

/* Pseudo-classes: position */
li:nth-child(odd) { background: #f8f8f8; }
tr:first-child th { border-top: 0; }
p:not(:last-child) { margin-bottom: 1rem; }

/* Pseudo-elements */
blockquote::before { content: '“'; font-size: 2em; }
blockquote::after { content: '”'; font-size: 2em; }
input::placeholder { color: #999; font-style: italic; }
::selection { background: #5BA3D8; color: white; }
Combinators + logical selectors·css
/* Combinators */
article p { line-height: 1.7; }         /* descendant */
nav > ul > li { display: inline-block; } /* direct child */
h2 + p { margin-top: 0.5rem; }          /* adjacent sibling */
h2 ~ p { font-size: 1.05rem; }          /* general sibling */

/* Logical selectors */
:is(h1, h2, h3) a { color: inherit; text-decoration: none; }
:where(.card, .panel, .tile) { border-radius: 8px; padding: 1rem; }
button:not([disabled]):hover { background: #5BA3D8; }
article:has(img) { background: #fafafa; padding: 1rem; }

/* Selector list */
h1, h2, h3, h4 { font-family: 'Inter', sans-serif; }

External links

Exercise

Open any complex web page (your favorite news site). Using only the browser DevTools, write a selector that targets: (1) every external link, (2) every paragraph inside an article, (3) every odd-numbered list item, (4) every form input that's invalid, (5) every article that contains an image. Test each by counting matches in DevTools console: $$('selector').length.
Hint
$$ is a DevTools alias for document.querySelectorAll. The .length tells you how many elements match. If you get 0 when you expected matches, your selector is probably looking for an attribute or pseudo-class state that isn't present at the moment.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.