🎨 CSS Fundamentals

CSS Selectors Cheatsheet: Every Selector You'll Actually Use

By Shubham Sharma··12 min read
CSS selector syntax printed on a colorful gradient background

Selectors are how CSS finds the elements you want to style — and the better you know them, the less BEM-style class soup you end up writing. When I audit a stylesheet at work, the first smell I look for is a wall of .card__header__title--large classes. Nine times out of ten, three well-chosen selectors would have replaced fifty class names. Here's the practical reference I actually use day to day — no obscure edge cases, just what ships in real production code.

Basic selectors

  • * — universal (rarely used except in resets)
  • tag — element type (h1, p, button)
  • .class — the workhorse
  • #id — id (avoid for styling; too specific)
  • [attr=value] — attribute (great for form inputs)

Combinators — the four you must memorise

  • A B — descendant (any depth)
  • A > B — direct child only
  • A + B — the very next sibling
  • A ~ B — any later sibling
nav a          { color: blue; }       /* every anchor inside a nav */
nav > a        { color: red; }        /* only direct-child anchors */
h2 + p         { margin-top: 0; }     /* p right after an h2 */
h2 ~ p         { color: gray; }       /* every p after an h2 */

Attribute selectors — my favourite hidden gem

a[href^="https"]      { /* starts with */ }
a[href$=".pdf"]       { /* ends with */ }
a[href*="youtube"]    { /* contains */ }
input[type="email"]   { /* exact match */ }

I use a[href^="https"]:not([href*="mydomain.com"])::after to add an external-link icon on every article on this very site — zero JavaScript.

Powerful pseudo-classes

/* every other row */
tr:nth-child(even) { background: #f7f7f7; }

/* first 3 items */
li:nth-child(-n+3) { font-weight: 600; }

/* style a card when it contains an image */
.card:has(img) { padding-top: 0; }

/* style inputs only after the user has interacted */
input:user-invalid { border-color: red; }

:has() — the parent selector we waited 20 years for

Available in every modern browser since late 2023, :has() lets you style an element based on its descendants. Before :has(), we had to add a wrapper class with JavaScript. Now the CSS just reacts.

/* form with an invalid field: highlight the whole card */
.card:has(input:invalid) { outline: 2px solid red; }

/* label whose input is checked */
label:has(input:checked) { background: #eef; }

:is() and :where() — group selectors without repeating

/* Instead of h1 a, h2 a, h3 a { ... } */
:is(h1, h2, h3) a { color: inherit; }

/* :where() is identical but zero specificity — great for resets */
:where(ul, ol) { padding-left: 1.5rem; }

Pseudo-elements — style parts that don't exist in HTML

  • ::before / ::after — insert generated content
  • ::first-line, ::first-letter — typographic touches
  • ::marker — style list bullets/numbers directly
  • ::placeholder — style the input placeholder text
  • ::selection — the highlight color when text is selected

Specificity in one sentence

Inline > id > class/attr/pseudo-class > tag/pseudo-element. When in doubt, use a slightly more specific selector — never !important. If you're reaching for !important weekly, something upstream in your architecture is wrong.

Frequently asked questions

Is :has() safe to use in production?

Yes — it's supported in Chrome, Edge, Safari, and Firefox since 2023. For older browsers you can wrap it in a @supports selector(:has(a)) block and provide a fallback.

Should I use id selectors?

Rarely. They have very high specificity (100 vs 10 for a class) and make CSS hard to override. IDs are still useful as scroll anchors and JavaScript hooks — just avoid them in your stylesheet.

What's the difference between :is() and :where()?

Functionally identical, but :where() has zero specificity, so anything you write later can override it. :where() is perfect for resets and base styles; :is() is better when you want the specificity to stick.

Why avoid the universal * selector?

It matches every element — including inside third-party widgets you didn't write. Modern resets like Josh Comeau's use it deliberately with a narrow ruleset (box-sizing, margin: 0), but avoid * for anything opinionated.

Enjoyed this article?

Share it with a fellow developer or explore more tutorials in our blog.

More articles

Related articles