Pseudo-classes and pseudo-elements — real use cases
A single colon selects a STATE an existing element can be in; a double colon selects a sub-part of an element that doesn't exist as its own node in the DOM at all — a distinction that explains what each category can and can't actually do.
3 min read
Pseudo-classes (:): a state the browser tracks, applied to a real element
a:hover { text-decoration: underline; } /* mouse is currently over this real <a> */
input:focus { outline: 2px solid blue; } /* this real <input> currently has focus */
li:nth-child(odd) { background: #f5f5f5; } /* this real <li> is at an odd position among its siblings */
button:disabled { opacity: 0.5; cursor: not-allowed; } /* this real <button> is in a disabled state */A pseudo-class selects a real, existing element — but only when that element is in a particular state: being hovered, having keyboard focus, occupying a particular position among its siblings, being disabled. Nothing new is created in the DOM; the selector just adds a condition on top of an ordinary element selector, which is why pseudo-classes can be combined with any other selector exactly like a class (button:disabled:hover is valid and means both conditions at once).
Pseudo-elements (::): a sub-part with no element of its own in the DOM
p::first-line { font-weight: bold; } /* targets just the first rendered LINE — not a real DOM node */
.quote::before { content: "\201C"; } /* generates a NEW box, not present in the DOM/HTML at all */
.quote::after { content: "\201D"; }::first-line selects a sub-part of <p>'s content that has no corresponding element in the actual DOM tree — the browser computes where the first rendered line ends based on the current layout and width, and styles just that portion. ::before and ::after go further: they generate an entirely new, styleable box as a child of the selected element, purely through CSS, with content controlled by the content property — genuinely new visual content that never appears in the page's actual markup or accessibility tree by default.
A real, common use: icon-free decorative content with ::before/::after
.required-field::after {
content: " *";
color: red;
}
.tooltip-trigger::after {
content: attr(data-tooltip); /* pulls its content from a data-* attribute on the real element */
position: absolute;
/* ...tooltip styling */
}Because content can be a literal string, a counter, or even attr() pulling from the real element's own attribute, ::before/::after are a common way to add small decorative or contextual content (a required-field asterisk, a tooltip's text) without needing extra markup in the HTML — purely presentational content that belongs in CSS rather than cluttering the document structure, provided it's genuinely decorative and not information a screen reader user actually needs (generated content is inconsistently exposed to assistive technology, so anything meaningful still belongs in real HTML).
:nth-child() beyond just odd/even: real formula-based selection
li:nth-child(3n) { border-top: 1px solid #ddd; } /* every 3rd item: 3, 6, 9, ... */
li:nth-child(n+4) { opacity: 0.6; } /* the 4th item ONWARD */
li:nth-child(-n+3) { font-weight: bold; } /* only the FIRST three items */:nth-child() accepts a genuine an+b formula, not just the odd/even keywords — 3n selects every third child, n+4 selects from the 4th child onward (since n starts at 0), and -n+3 selects only the first three (counting backward from a large starting point down to the 3rd). This formula-based selection covers a surprising range of real layout needs — "highlight every third row," "only style the first 3 items differently" — entirely in CSS, with no class needing to be added per-item in the markup or via JavaScript.
Further reading
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. What's the fundamental difference between a pseudo-class (`:hover`) and a pseudo-element (`::before`)?
2. What does `.quote::before { content: "\201C"; }` actually do?
3. What does `li:nth-child(n+4)` select?