CSS Cheat Sheet: Selectors, Specificity, and the Properties Worth Memorising
Print it. The left column is a spec fact; the right column is what you actually look up.
Specificity, once
Four-part count (a, b, c, d), compared left to right:
| Adds | Example | Weight |
|---|---|---|
a — ID selectors | #nav | 1,0,0,0 |
b — class, attribute, pseudo-class | .btn, [type=button], :hover, :not() contents count | 0,1,0,0 |
c — type and pseudo-element | div, p, ::before | 0,0,1,0 |
d — universal / combinator | *, +, >, ~, | 0,0,0,0 |
Inline styles beat all of it except !important; !important reverses normal order, and an !important in a stylesheet beats an inline !important. A :where() wrapper contributes zero — that is why reset libraries use it. Cascade order is decided before specificity: origin, then @layer, then specificity, then source order. Specificity loses to layers, which surprises people who have read the specificity table and stopped.
Selectors you need
| Selector | Matches |
|---|---|
.a.b | both classes on one element |
.a .b | any descendant |
.a > .b | direct child only |
.a ~ .b | later sibling, any distance |
.a + .b | immediately next sibling |
:is(.a, .b) .c | takes the specificity of its most specific argument |
:where(.a, .b) .c | contributes zero specificity |
:not(.a) | anything not .a; contents still count toward specificity |
:has(> input:checked) | parent based on child — the selector CSS never had |
p::first-line, ::selection | pseudo-elements; they add to c, not b |
[attr=value i] | attribute match, case-insensitive |
Box model and layout
*, *::before, *::after { box-sizing: border-box; } /* width = total width */
.parent { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 1rem; }
.parent { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; }
.center { display: grid; place-items: center; } /* both axes, one line */
.stack { display: flex; flex-direction: column; gap: .75rem; } /* margins not needed */
.scroll { overflow: auto; overscroll-behavior: contain; }
.clamp { width: min(92vw, 68ch); margin-inline: auto; } /* fluid, capped */
.trunc { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.lines { display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
gap works on flex as well as grid. Percentage heights need a definite parent height; min-height: 100dvh on an ancestor fixes most "my flex child will not stretch" reports, and dvh avoids the mobile URL-bar jump that vh has.
Typography and space
html { font-size: 100%; } /* rem = user's preference */
body { line-height: 1.5; max-width: 68ch; }
h1 { font-size: clamp(1.6rem, 1.2rem + 2vw, 2.6rem); text-wrap: balance; }
p { text-wrap: pretty; hyphens: auto; }
.gap { margin-block: 0 1rem; } /* logical property: works in RTL */
text-wrap: balance and pretty are supported in current Safari, Chrome, and Firefox; without them they are inert declarations, so they are free to add today.
Color and state
:root { --surface: light-dark(#f7f8fa, #0d1117); --accent: oklch(70% .16 245); color-scheme: light dark; }
.btn:is(:hover, :focus-visible) { background: color-mix(in oklab, var(--accent) 82%, transparent); }
.btn:disabled { cursor: not-allowed; opacity: .55; }
color-mix() gives you hover states without a preprocessor. :focus-visible is the correct focus hook — :focus fires on mouse click and makes pages look broken.
@-rules
@media (prefers-color-scheme: dark) { … }
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: .01ms !important; transition-duration: .01ms !important; } }
@media (width >= 48rem) { … } /* range syntax, no min/max-width */
@layer base, components, utilities; /* declaration order = priority order */
@supports (display: grid) and (not (-moz-display: grid)) { … }
@container (min-width: 30rem) { … } /* needs container-type: inline-size */
What a formatter may and may not touch
Safe: whitespace outside tokens, comments you asked to drop. Never safe to automate:
!important— semantic, not decoration.- Duplicate selectors — merging them changes behaviour with equal specificity if the later rule's declarations differ; a minifier that merges can break a page.
- Whitespace inside a quoted string or inside
url()without quotes — that is content. A formatter doing this is producing valid-looking CSS with corrupted values; see minify safety. - Vendor prefix order —
-webkit-must precede the unprefixed declaration or the unprefixed one wins.
The beautifier and minifier here operate on whitespace and comments only; the linked page lists exactly where that rule breaks, measured.