CSS Selectors

A CSS selector is the part of a rule that tells the browser which elements to style. Everything you do in CSS starts with selecting the right elements — get the selector wrong, and your carefully written declarations never apply, or apply to the wrong thing. Selectors range from trivially simple (target every <p>) to highly specific (target only the third <li> inside a <nav> that has a certain attribute). Understanding the full selector vocabulary, and how the browser decides which competing rule wins, is one of the most valuable skills in CSS.

Overview / How it works

When the browser parses your stylesheet, it builds a list of rules, each made of one or more selectors paired with a block of declarations. As it renders the page, for every element in the DOM the rendering engine asks: “which of my rules match this element, and if more than one matches, which one wins?” Selectors are how a rule announces which elements it matches. Some selectors match by tag name (<code>p</code>), some by an attribute the element carries (<code>class</code>, <code>id</code>, <code>data-*</code>), some by the element’s position among its siblings, and some by a temporary state such as being hovered or focused.

Selectors can also be combined with combinators to describe relationships between elements: descendant, direct child, adjacent sibling, and general sibling. This lets a single rule reach deep into a specific part of the document tree without adding extra classes to every element.

When two or more rules match the same element and set the same property, the browser must pick a winner. This is resolved by a well-defined algorithm: first by origin and importance (author styles beat user-agent defaults, <code>!important</code> flips the order), then by specificity (a score computed from the selector’s composition), and finally by source order (the last rule declared wins a tie). Mastering selectors means mastering not just how to write them, but how to predict which one the browser will actually apply.

Syntax

selector { property: value; }

selector1, selector2 { property: value; }

selector1 selector2 { property: value; }
  • selector — one or more patterns describing which elements to match.
  • , (comma) — groups multiple, unrelated selectors so they share the same declaration block.
  • (space) — the descendant combinator: matches selector2 anywhere inside selector1, at any depth.
  • { } — the declaration block, containing one or more <code>property: value;</code> pairs.
Selector Matches Example
Type Every element with that tag name p
Class Every element with that class attribute .warning
ID The single element with that id #main-nav
Universal Every element *
Attribute Elements carrying (or matching) an attribute [type="text"]
Descendant B anywhere inside A article p
Child B directly inside A ul > li
Adjacent sibling B immediately after A h2 + p
General sibling B after A, same parent h2 ~ p
Pseudo-class Element in a certain state/position a:hover, li:first-child
Pseudo-element A generated sub-part of an element p::first-line

Examples

Example 1: Class, ID, and grouping

#site-header {
  background-color: #1f2937;
  color: #ffffff;
  padding: 1rem 2rem;
}

.badge {
  display: inline-block;
  padding: 0.15rem 0.5rem;
  border-radius: 999px;
  font-size: 0.75rem;
}

h1, h2, h3 {
  font-family: "Georgia", serif;
  line-height: 1.2;
}

Result: The single element with <code>id=”site-header”</code> gets a dark background, white text, and padding. Any element carrying <code>class=”badge”</code> (there can be many) becomes a small inline pill shape. All <code>h1</code>, <code>h2</code>, and <code>h3</code> elements share the same serif font and tightened line spacing, because the comma groups three independent type selectors onto one rule.

This shows the three most common selector categories working together: an ID for a unique landmark, a class for a reusable pattern that can appear many times, and grouped type selectors to avoid repeating identical declarations three times.

Example 2: Combinators for structural targeting

nav > ul > li {
  display: inline-block;
  margin-right: 1.5rem;
}

nav a:hover {
  text-decoration: underline;
}

h2 + p {
  font-weight: bold;
  color: #374151;
}

.card ~ .card {
  margin-top: 1rem;
}

Result: Only <code>li</code> elements that are direct children of a <code>ul</code> that is itself a direct child of <code>nav</code> become inline horizontal menu items — an <code>li</code> nested inside a sub-list is untouched. Any link inside <code>nav</code> underlines on hover. The very first paragraph immediately following an <code>h2</code> is bolded and dark gray (a common “lead paragraph” pattern), while later paragraphs are unaffected. Every <code>.card</code> that has another <code>.card</code> before it (a sibling, not necessarily adjacent) gets top margin, so the first card in a stack stays flush while the rest are spaced out.

Combinators let you describe relationships instead of tagging every element with a class. The child combinator (<code>></code>) is stricter than the descendant combinator (space) — it stops at one level, which avoids accidentally styling deeply nested lookalike elements.

Example 3: Attribute and pseudo-class selectors on a real form

input[type="email"],
input[type="password"] {
  border: 1px solid #d1d5db;
  border-radius: 4px;
  padding: 0.5rem;
}

input:focus {
  outline: 2px solid #2563eb;
  outline-offset: 2px;
}

input:invalid:not(:placeholder-shown) {
  border-color: #dc2626;
}

li:nth-child(odd) {
  background-color: #f9fafb;
}

Result: Email and password fields get a light gray rounded border and internal padding. Whichever field currently has keyboard/mouse focus shows a visible blue outline (important for accessibility). An input that fails its own validation (e.g. a malformed email) turns its border red, but only once the user has typed something — <code>:not(:placeholder-shown)</code> excludes the untouched, empty field so it doesn’t show an error before the user has had a chance to type. Every odd-numbered <code>li</code> in any list gets a subtle striped background.

This example combines attribute selectors (matching a specific <code>type</code> value), state-based pseudo-classes (<code>:focus</code>, <code>:invalid</code>), the negation pseudo-class (<code>:not()</code>), and a structural pseudo-class (<code>:nth-child()</code>) — all without a single extra class in the HTML.

How it works step by step: specificity

When more than one rule targets the same element and property, the browser computes a specificity score for each matching selector as a triple of (ID count, class/attribute/pseudo-class count, type/pseudo-element count):

  • ID selectors (<code>#id</code>) each contribute 1 to the first column.
  • Class, attribute, and pseudo-class selectors (<code>.class</code>, <code>[attr]</code>, <code>:hover</code>) each contribute 1 to the second column.
  • Type and pseudo-element selectors (<code>div</code>, <code>::before</code>) each contribute 1 to the third column.
  • The universal selector (<code>*</code>), combinators (<code>></code>, <code>+</code>, <code>~</code>, space), and <code>:where()</code> contribute nothing.

Consider <code>nav ul li.active a</code>: three type selectors (<code>ul</code>, <code>li</code>, <code>a</code> — wait, <code>nav</code> is a fourth type selector) and one class (<code>.active</code>), giving a score of (0, 1, 4). Compare that against <code>#sidebar a</code>, which scores (1, 0, 1). The browser compares column by column, left to right: the ID column of (1,0,1) beats the ID column of (0,1,4) — 1 is greater than 0 — so <code>#sidebar a</code> wins regardless of how many classes or types the other selector piles on. This is why a single ID selector so often “beats” a long chain of classes, and why relying on ID selectors for styling makes overrides painful later. Inline <code>style</code> attributes outrank every selector-based rule, and <code>!important</code> outranks even that (except another, later <code>!important</code>). If two rules have identical specificity, the one that appears later in the stylesheet (or later in document order for linked stylesheets) wins — this is the “cascade” tie-breaker.

Common Mistakes

Mistake: over-qualifying a selector.

div.container ul.nav-list li.nav-item a.nav-link {
  color: blue
}

This is a real ruleset but it’s a maintenance trap: it’s needlessly specific (five compound selectors deep), so any later attempt to override just the link color needs an equally specific — or more specific — selector, or <code>!important</code>. It’s also missing the semicolon after <code>blue</code>, which some parsers tolerate on the last declaration but is easy to trip over once you add a second declaration. Prefer a single class:

.nav-link {
  color: blue;
}

Mistake: confusing the descendant and child combinators.

.menu li {
  border-bottom: 1px solid #eee;
}

If <code>.menu</code> contains a nested submenu (<code>ul>li>ul>li</code>), this rule — using the descendant combinator (a space) — applies to the nested <code>li</code> elements too, which is usually not intended. Using the child combinator restricts the rule to only the top-level items:

.menu > li {
  border-bottom: 1px solid #eee;
}

Best Practices

  • Favor classes over IDs for styling; reserve IDs for JavaScript hooks, fragment links, and form label associations.
  • Keep specificity low and flat — a long, deeply nested selector chain is hard to override later.
  • Use the child combinator (<code>></code>) instead of the descendant combinator when you specifically mean “direct child,” to avoid unintentionally matching nested structures.
  • Group selectors with commas to avoid duplicating identical declaration blocks.
  • Avoid <code>!important</code> except as a last resort (e.g. overriding third-party CSS you can’t edit) — it breaks the normal cascade and specificity model.
  • Use attribute selectors (<code>[type=”checkbox”]</code>) to target semantic HTML states instead of adding redundant classes.
  • Use <code>:where()</code> when you want a selector’s matching power without adding to its specificity, which keeps overrides easy for consumers of a shared stylesheet.

Practice Exercises

Exercise 1: Write a rule that gives every direct-child <code>li</code> of an element with class <code>.tabs</code> a bottom border, but leaves any <code>li</code> nested inside a sub-list untouched.

Exercise 2: Given the rules <code>.button { color: black; }</code> and <code>#submit-btn { color: white; }</code> both applying to the same <code><button id=”submit-btn” class=”button”></code>, determine which color wins and explain why using specificity scoring.

Exercise 3: Write a single selector that targets any <code>input</code> element that is both required and currently invalid, and gives it a red left border, without using a class.

Summary

  • Selectors determine which elements a rule’s declarations apply to; combinators (space, <code>></code>, <code>+</code>, <code>~</code>) describe relationships between elements.
  • Pseudo-classes target state or structural position (<code>:hover</code>, <code>:nth-child()</code>); pseudo-elements target generated sub-parts (<code>::before</code>).
  • When rules conflict, the browser resolves the winner by origin/importance, then specificity (ID, class/attribute/pseudo-class, type/pseudo-element counts compared column by column), then source order.
  • Low, flat specificity built from classes is easier to maintain than deep chains or ID-based selectors.
  • <code>!important</code> and inline styles override normal specificity rules and should be used sparingly.