CSS Pseudo-Classes vs Pseudo-Elements Explained
Photo by Unsplash on Unsplash
Table of Contents
The Short Version
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
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
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
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
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 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
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
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