CSS Selectors Cheat Sheet
A practical tour of the selectors that cover 90% of real stylesheets: the basics (type, class, id, attribute), combinators for relationships in the DOM tree, the modern pseudo-classes (`:is`, `:where`, `:not`, `:nth-child`), the most common pseudo-elements (`::before`, `::after`, `::placeholder`), and a quick specificity reference so you can predict which rule wins. Skim it once to set the mental model, then come back when a hover style refuses to apply.
257 views
4
/* Type selector: every <button> on the page. */
button {
padding: 0.5rem 1rem;
}
/* Class selector: any element with class="primary". */
.primary {
background: royalblue;
color: white;
}
/* ID selector: the (unique) element with id="checkout". */
#checkout {
border: 2px solid limegreen;
}
/* Attribute selectors: presence, exact match, prefix, suffix, substring. */
[data-state] { outline: 1px dashed gray; } /* has the attribute */
input[type="email"] { border-color: dodgerblue; } /* exact match */
a[href^="https://"] { color: green; } /* starts with */
img[src$=".svg"] { background: none; } /* ends with */
link[rel*="icon"] { /* substring match */ }
/* Combinators describe the DOM relationship between two selectors. */
.card p { line-height: 1.5; } /* descendant: any p inside .card */
.card > p { font-weight: 600; } /* child: direct child only */
h2 + p { margin-top: 0; } /* adjacent sibling: p RIGHT after h2 */
h2 ~ p { color: dimgray; } /* general sibling: any p after h2 */These four families (type, class, id, attribute) plus the four combinators are enough to target almost any element you can describe with a single sentence. Attribute selectors are underused: [type="email"] is far more robust than .email-input because it follows the actual semantic of the input rather than a class name that can rot. The descendant combinator is whitespace; the child combinator is >; the adjacent sibling is +; the general sibling is ~. Combinators read left-to-right but the browser actually matches right-to-left, which is why deep selectors are slower; keep them shallow when you can.
/* State pseudo-classes target user interaction. */
button:hover {
cursor: pointer;
}
button:focus-visible {
/* :focus-visible only triggers for keyboard focus, not mouse clicks. */
outline: 2px solid orange;
outline-offset: 2px;
}
input:disabled { opacity: 0.5; }
input:checked + label { font-weight: 700; }
/* Structural pseudo-classes target position in the parent. */
li:first-child { border-top: none; }
li:last-child { border-bottom: none; }
li:nth-child(odd) { background: #f5f5f5; } /* zebra rows */
li:nth-child(2n + 1) { /* same as odd, formula form */ }
li:nth-of-type(3) { /* third <li> sibling, ignoring other tags */ }
/* :is() and :where() group selectors. They differ ONLY in specificity. */
:is(h1, h2, h3) { font-family: 'Inter', sans-serif; }
/* :is bumps specificity to its most specific argument. */
:where(article, section, aside) p { line-height: 1.6; }
/* :where always contributes 0 specificity, perfect for low-specificity defaults. */
/* :not() excludes matches. Combine with :is for readable selectors. */
button:not(:disabled):not(.ghost) {
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.1);
}State pseudo-classes (:hover, :focus-visible, :disabled, :checked) and structural ones (:first-child, :nth-child, :nth-of-type) cover the vast majority of dynamic styling needs. Use :focus-visible instead of :focus for keyboard outlines so mouse clicks do not get a halo. The newer :is(), :where(), and :not() group and exclude selectors; the key difference is specificity: :is adopts the highest specificity of its arguments, while :where always contributes zero, which makes it ideal for low-specificity baseline rules that callers can override with a single class. :nth-child(2n + 1) and :nth-child(odd) are equivalent; pick whichever reads clearer for the rule at hand.
/* ::before and ::after generate content. content is required, even if empty. */
.required-field::after {
content: " *";
color: crimson;
font-weight: 700;
}
.tooltip::before {
content: "";
position: absolute;
inset: -4px;
border: 1px dashed currentColor;
border-radius: inherit;
pointer-events: none;
}
/* ::first-letter and ::first-line target inline-level slices of text. */
p.lead::first-letter {
font-size: 3rem;
float: left;
line-height: 1;
margin-right: 0.25rem;
}
/* ::placeholder styles the placeholder hint inside an input. */
input::placeholder {
color: gray;
font-style: italic;
}
/* ::selection styles user-highlighted text. */
::selection {
background: gold;
color: black;
}
/* Older single-colon syntax still works for these four legacy pseudo-elements,
but the modern double-colon form is the recommended spelling:
:before, :after, :first-letter, :first-line. */Pseudo-elements address parts of an element the DOM does not expose directly: the imaginary boxes before and after content (::before, ::after), the first letter or line of a paragraph, the placeholder of an input, the user's selection. The content property is mandatory on ::before and ::after; without it, the browser will not generate the box. The double-colon syntax distinguishes pseudo-elements from pseudo-classes (single colon) and is the standard going forward. Keep generated content presentational, not informational, because screen readers may skip it; never put critical text inside content.
/* The simplified rule: count (inline=1000, id=100, class/attr/pseudo-class=10,
element/pseudo-element=1). Higher count wins; ties go to the later rule. */
/* Specificity 0,0,0,1 (one element). */
button { color: gray; }
/* Specificity 0,0,1,0 (one class). Beats the rule above. */
.danger { color: red; }
/* Specificity 0,0,2,1 (two classes + one element). Beats both above. */
button.danger.large { color: crimson; }
/* Specificity 0,1,0,0 (one id). Beats every rule above. */
#submit { color: white; }
/* :where() neutralizes specificity for default styles other rules can override. */
:where(.btn) { padding: 0.5rem; } /* contributes 0,0,0,0 */
.btn { padding: 1rem; } /* 0,0,1,0 wins because :where is 0 */
/* :is() raises specificity to its highest argument. */
:is(.alert, #urgent) p { color: black; } /* counts as 0,1,0,0 because of #urgent */
/* Avoid !important except for accessibility / utility overrides; once it is in
the cascade, the only way to beat it is another !important, which spirals. */
.visually-hidden {
position: absolute !important;
}Specificity is the tiebreaker when two rules target the same element with conflicting declarations. Memorize the four-component count and you can predict which rule wins without opening DevTools. The modern best practice is to keep selectors flat (one class is usually enough) and use :where() for resets and base styles so authoring code can override them with a single class. !important should be a last resort because it removes you from the cascade entirely; the only legitimate uses are utility classes that must always win (.sr-only, .hidden) and accessibility overrides like prefers-reduced-motion resets.
