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.

Intermediate

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?