Skip to main content
TF
By Rohit V.7 min readArticle

CSS Pseudo-Classes vs Pseudo-Elements Explained

TF
ToolsFuel Team
Web development tools & tips
CSS Pseudo-Classes vs Pseudo-Elements Explained

Photo by Unsplash on Unsplash

The Short Version

Pseudo-classes and pseudo-elements are two different CSS features with confusingly similar names. A pseudo-class styles an element based on its *state* or position — like `:hover` when the mouse is over it, or `:first-child` when it's the first of its siblings. A pseudo-element styles a *specific part* of an element, or injects new content — like `::first-line` to style just the opening line, or `::before` to add a decorative bit ahead of the content.

The quick mental model I use: a pseudo-class answers 'what state is this element in?' while a pseudo-element answers 'which piece of this element am I targeting, or what am I adding to it?' They even have different syntax — one colon versus two — which is the tell that instantly reveals which one you're looking at. Get that distinction straight and a big chunk of CSS selector confusion just melts away.

The One-Colon vs Two-Colon Rule

Here's the syntax rule worth burning into memory: pseudo-classes use a single colon (`:hover`, `:nth-child(2)`), while pseudo-elements use a double colon (`::before`, `::after`, `::first-line`). The double colon was introduced specifically to distinguish pseudo-elements from pseudo-classes, and it's the fastest way to tell them apart at a glance.

There's a historical wrinkle: the original CSS versions used a single colon for both, so you'll still see old code with `:before` and `:after`. Browsers keep supporting that single-colon form for those four legacy pseudo-elements for backward compatibility, which is exactly why the confusion persists. My advice is to always write the modern double colon for pseudo-elements in new code. It's correct, it signals intent clearly, and it keeps you from mixing up the two categories in your own head. When you see two colons, you know instantly you're dealing with a piece or a generated bit, not a state.

Pseudo-Classes: Styling by State

Pseudo-classes react to conditions the element is in, many of them dynamic. The interactive ones are the everyday heroes: `:hover` fires when the pointer is over an element, `:focus` when it's selected via keyboard or click, and `:active` while it's being pressed. These power almost every button and link effect you've ever seen.

Then there are structural pseudo-classes that target elements by their position in the document tree: `:first-child`, `:last-child`, and the powerful `:nth-child()`, which can select every other row, every third item, and so on. Others reflect form state, like `:checked`, `:disabled`, and `:valid`. What unites them all is that they don't create anything new — they select an existing element based on a condition. If the condition is true, the styles apply; if it changes, the styles react. That reactivity is what makes pseudo-classes feel almost like logic inside your stylesheet.

Pseudo-Elements: Styling a Part or Adding Content

Pseudo-elements work differently — they let you target a sub-part of an element or generate brand-new content that doesn't exist in your HTML. The two you'll use most are `::before` and `::after`, which insert generated content immediately before or after an element's actual content. They require a `content` property to render, even if it's just an empty string, and they're perfect for decorative icons, tooltips, and clearfix tricks.

Others target existing text with surgical precision: `::first-line` styles only the first line of a paragraph, `::first-letter` styles the opening character (great for drop caps), `::selection` styles highlighted text, and `::placeholder` styles the faint prompt text in an input. The theme is consistent: a pseudo-element addresses a portion of an element or manufactures a new bit attached to it, rather than reacting to a whole element's state. That's the core split from pseudo-classes, and it holds up across every example.

A Side-by-Side That Makes It Click

Put them next to each other and the difference is obvious. `a:hover` styles a whole link when you hover it — that's a pseudo-class reacting to state. `a::after` adds a little arrow after a link's text — that's a pseudo-element generating content. Same element, two totally different jobs, revealed instantly by the colon count.

Here's a practical combo you'll use constantly: `li::before { content: '→ '; }` adds an arrow before every list item, while `li:first-child { font-weight: bold; }` bolds only the first item. One manufactures content on every item; the other selects a single item by position. You can even chain them — `p:hover::first-line` styles the first line of a paragraph only while it's hovered — which shows they're genuinely separate mechanisms that compose together. If you're building layouts where these selectors matter, my guide on
Flexbox vs Grid pairs well for the structural side of things.

Common Mistakes and Gotchas

The number-one mistake is forgetting the `content` property on `::before` and `::after`. Without it — even an empty `content: ''` — the pseudo-element simply won't render. Nothing appears, and developers burn ages wondering why their decorative element vanished. If a `::before` isn't showing, check for a missing `content` first.

The second common trip-up is the colon count itself. Writing `:before` still works for the four legacy pseudo-elements, but writing `::hover` (a pseudo-class with two colons) does not work at all, because `:hover` was never a pseudo-element. So the rule is asymmetric: legacy pseudo-elements tolerate one colon, but pseudo-classes strictly require one. When something silently fails, counting colons is a shockingly effective debugging step. And remember that generated content from pseudo-elements isn't in the DOM, so it can't be selected by JavaScript or, in most cases, copied by users — great for decoration, wrong for anything meaningful.

When to Reach for Each

Choosing between them is usually obvious once you frame the question correctly. Ask: am I reacting to a state or position, or am I styling a part or adding decoration? If it's the former — hover effects, form validation styling, striping every other row — you want a pseudo-class. If it's the latter — an icon before a heading, a custom bullet, styling just the first letter — you want a pseudo-element.

A good instinct is to reach for pseudo-elements for anything purely decorative, because keeping decoration out of your HTML keeps your markup clean and semantic. Icons, separators, quote marks, and overlays all belong in `::before` and `::after` rather than as empty `<span>` tags cluttering your document. Pseudo-classes, meanwhile, are your tool for interactivity and structure. Used well, the two together let you build rich, responsive interfaces with far less markup than you'd otherwise need. And if you're sizing that generated content, getting your
CSS units right matters just as much as picking the correct selector.

The Quick Reference

To lock it in, here's the whole thing in a nutshell. Pseudo-classes use one colon and style an element based on state or position: `:hover`, `:focus`, `:nth-child()`, `:checked`. Pseudo-elements use two colons and style a part of an element or inject content: `::before`, `::after`, `::first-line`, `::selection`, `::placeholder`.

When you're reading unfamiliar CSS, let the colons guide you — one means state, two means a piece or generated content. When you're writing it, always use double colons for pseudo-elements in new code, and never forget the `content` property on `::before` and `::after`. The official
MDN reference on pseudo-elements is the canonical place to confirm any specific one. Master this single distinction and you've cleared up one of the most persistent points of confusion in all of CSS.

Frequently Asked Questions

What is the difference between a pseudo-class and a pseudo-element?

A pseudo-class styles an element based on its state or position, like :hover or :first-child. A pseudo-element styles a specific part of an element or injects new content, like ::before or ::first-line. Pseudo-classes react to conditions; pseudo-elements target a piece or generate content.

Why do some use one colon and others use two?

Pseudo-classes use a single colon (:hover) and pseudo-elements use a double colon (::before). The double colon was added to distinguish pseudo-elements. Older CSS used a single colon for both, so legacy pseudo-elements like :before still work with one colon for backward compatibility.

Why isn't my ::before or ::after showing up?

The most common reason is a missing content property. Pseudo-elements like ::before and ::after won't render without it, even if it's an empty string (content: ''). Always include the content property first when a generated pseudo-element doesn't appear.

Can I use both a pseudo-class and pseudo-element together?

Yes. You can chain them, such as p:hover::first-line, which styles the first line of a paragraph only while it's hovered. They're separate mechanisms that compose together, letting you combine state-based conditions with part-based targeting in a single selector.

Should I write :before or ::before?

Use the double-colon ::before in new code. It's the modern, correct syntax that clearly marks a pseudo-element. The single-colon :before still works for four legacy pseudo-elements, but writing the double colon signals intent and prevents confusion. See my guide on [Flexbox vs Grid](/blog/css-flexbox-vs-grid-when-to-use-which) for more CSS layout help.

Try ToolsFuel

23+ free online tools for developers, designers, and everyone. No signup required.

Browse All Tools