A CSS selector cheat sheet earns its bookmark by being faster to scan than a search engine. This one is organized the way you'd actually reach for it mid-build: basic selectors first, then combinators, then the pseudo-classes and attribute selectors that cover almost every real-world case, with a live tester at the end for when you want to confirm a selector actually matches what you think it matches before shipping it.

A magnifying glass over a sketched tree of HTML angle-bracket tags, one node glowing highlighted
A magnifying glass over a sketched tree of HTML angle-bracket tags, one node glowing highlighted

Basic selectors

SelectorExampleMatches
TypepEvery p element
Class.cardEvery element with class card
ID#headerThe element with id header
Universal*Every element
Groupingh1, h2, h3Any element matching any listed selector

Grouping selectors with a comma applies the same rule to each without repeating it, useful for consistent heading styles or shared reset rules, but keep an eye on it as a codebase grows: a long comma-separated list is often a sign the rule belongs on a shared class instead.

Combinators

Combinators describe the relationship between two selectors, not just a single element in isolation.

CombinatorExampleMatches
Descendant (space).nav aAny a anywhere inside .nav, at any depth
Child.nav > liOnly li elements that are direct children of .nav
Adjacent siblingh2 + pA p immediately following a h2, same parent
General siblingh2 ~ pAny p that comes after a h2 and shares the same parent

The descendant combinator (a plain space) is the one people reach for by default, but it's also the least specific, it matches at any depth, which can grab elements you didn't intend once markup gets nested a few levels deep. The child combinator is worth defaulting to instead whenever you actually mean "direct child," since it fails loudly (nothing matches) instead of silently grabbing something two levels down.

Pseudo-classes

Pseudo-classes select elements based on a state or position rather than a static attribute.

Pseudo-classMatches
:hoverWhile the pointer is over the element
:focusWhile the element has keyboard focus
:focus-visibleFocus shown only when it's keyboard-triggered, not a mouse click
:first-child / :last-childAn element that is the first or last child of its parent
:nth-child(n)The nth child; supports formulas like :nth-child(2n) for every other element
:not(selector)Any element that doesn't match the selector inside the parentheses
:is(selector-list)Matches any selector in the list; shorthand for a group without repeating the rest of the selector
:where(selector-list)Same matching as :is(), but contributes zero specificity
:has(selector)An element that contains something matching the inner selector, the "parent selector" CSS lacked for years
:disabled / :checkedForm elements in a disabled or checked state

:has() is worth calling out specifically: it's the selector that finally lets you style a parent based on what's inside it, a card with an image versus one without, a form with an invalid field, without JavaScript. It reached Baseline "newly available" in late 2023 and, as of 2026, has effectively 100% support across Chrome, Edge, Firefox, and Safari, so you can ship it in production with no fallback. The same is now true of the other selectors on this sheet that used to need a caveat: :is(), :where(), and native CSS nesting all crossed into Baseline in 2023 and are safe defaults today rather than progressive enhancements. When you're unsure whether a specific selector is production-safe, the honest check is its Baseline status, not "does it work in my browser."

:is() and :where() both exist to shorten repetitive selector lists, and the difference between them is specificity: :where() always counts as zero, which makes it the safer choice inside a reset or a design system where you don't want to fight specificity wars with the components that consume it later.

Pseudo-elements

Pseudo-elements target a specific part of an element rather than the whole thing, and use a double colon in modern syntax.

Pseudo-elementTargets
::before / ::afterGenerated content inserted before or after an element's actual content
::first-lineThe first rendered line of a block of text, recalculated on resize
::first-letterThe first letter of a block, commonly used for drop caps
::placeholderThe placeholder text inside an input
::selectionThe portion of text currently highlighted by the user

Attribute selectors

Attribute selectors match based on the presence or value of an HTML attribute, no class or ID required.

SelectorMatches
[disabled]Any element with a disabled attribute, regardless of value
[type="text"]Elements where the attribute equals exactly this value
[href^="https"]Attribute value starts with this string
[href$=".pdf"]Attribute value ends with this string
[class*="btn"]Attribute value contains this string anywhere
[lang|="en"]Value equals this string, or starts with it followed by a hyphen (locale matching)

[href^="https"] is a genuinely useful one in practice: pair it with ::after to automatically add an external-link icon to every outbound link on a page, without touching the markup for each one individually.

Combining selectors

Most real-world selectors aren't a single piece from the tables above, they're several combined into one chain, and reading a combined selector left to right is the fastest way to understand what it does. nav a.active:not(:last-child) reads as: any a with class active inside a nav, that isn't the last child of its parent. Building selectors this way, one clause at a time, is easier to reason about than trying to write the whole thing from memory in one pass, and it's easier to debug later, since you can delete clauses one at a time to isolate which part of the chain isn't matching what you expect.

Common mistakes worth avoiding

  • Over-qualifying a selector. div.card works, but so does .card alone, and the extra type selector only adds fragility if the markup ever changes from a div to something else.
  • Reaching for !important instead of fixing specificity. A style that "won't apply" is almost always a specificity or ordering problem, not a browser bug, and !important tends to just relocate the problem to the next override down the line.
  • Using an ID selector for styling. IDs carry high specificity that's hard to override later, which is exactly the property you don't want in a shared stylesheet. Reserve IDs for JavaScript hooks and anchor links, and use classes for styling.
  • Forgetting that :nth-child() counts all children, not just matching ones. li:nth-child(2) inside a mixed list of li and other elements counts position across every child, not just the li elements, a common source of "why did it skip an item" confusion.

Specificity, in one paragraph

When two rules target the same element, the more specific one wins, and specificity is calculated roughly as: inline styles beat IDs, IDs beat classes and attribute selectors and pseudo-classes, and those beat plain type selectors. :where() is the one exception, it matches like a normal selector but adds nothing to that count. When a style "isn't applying" and the selector looks right in a quick read, specificity is the first thing worth checking, before assuming the browser is wrong.

Handy real-world recipes

The tables above are the vocabulary; these are the sentences you'll actually write most often, worth keeping within reach:

GoalSelector
Style a card only when it contains an image.card:has(img)
Flag a form field that's invalid and has been touchedinput:user-invalid
Add spacing between stacked items but not before the first.stack > * + *
Zebra-stripe every other table rowtr:nth-child(even)
Target external links onlya[href^="http"]:not([href*="yourdomain.com"])
Style a label whose checkbox is checkedinput:checked + label

The .stack > * + * pattern (often called the "lobotomized owl") is the one worth memorizing: it applies a style to every child except the first, which is exactly how you add consistent vertical rhythm to a stack of elements without a trailing margin hanging off the last one. It reads as "any element that immediately follows a sibling," so the first child, having nothing before it, is skipped.

Confirm a selector before you ship it

Reading a selector and knowing exactly what it matches gets harder as markup gets more nested, and a selector that looks right can still grab an extra element you didn't intend, or miss the one you meant. Rather than guessing from the CSS alone, our free CSS selector tester lets you paste real HTML and a selector, and see every matched element highlighted live in a rendered preview, along with the specificity breakdown, in your browser, nothing you paste is sent anywhere.

This is also the exact mechanic behind how Shotline scopes a piece of feedback to one specific element on a live page: a click generates a precise selector under the hood, the same kind of selector this cheat sheet covers, so a comment or a bug report is tied to the actual DOM node, not a vague description of where it is on the page. If you're wiring up a coding agent to act on that kind of feedback directly, how to add MCP servers to Claude Code covers the setup, and Playwright MCP with Claude Code covers the closely related case of giving an agent direct browser control.

Where Shotline fits

Every comment left through Shotline carries the CSS selector of the exact element it's pinned to, generated automatically the moment someone clicks, so whoever fixes it, a person or a coding agent working the queue, starts from the real DOM node instead of reconstructing it from a screenshot. If you're building the kind of agent-driven workflow this cheat sheet supports, what is vibe coding covers where that workflow tends to break down without a way to see the rendered page.

Shotline is free to try for 14 days, no card required, then from $19/mo (billed annually; $25 month-to-month). See it on the Shotline homepage or start a free trial.