Up to now, every selector you’ve written has targeted a real element — a <p>, a <div>, something that actually exists in your HTML. Pseudo-elements break that rule in a useful way: they let you style a part of an element, or even create brand-new visual content, without writing a single extra tag in your markup.
Want to add a decorative icon before a heading? A custom quote mark around a blockquote? Style just the first letter of a paragraph like an old book? Add a “required” asterisk after a form label? That’s all pseudo-elements. They’re one of those features that, once they click, you reach for constantly. Let’s break them down.
What a pseudo-element actually is
A pseudo-element targets a specific portion of an element rather than the whole thing. The browser treats that portion as if it were its own little element you could style — even though it’s not in your HTML.
You write them with a double colon (::), which is what visually distinguishes them from pseudo-classes (which use a single colon, like :hover):
p::first-line {
font-weight: bold;
}
That rule bolds only the first line of every paragraph — a chunk of text that has no tag of its own. You couldn’t select it any other way.
Double colon vs single colon
Pseudo-elements use two colons (::before), pseudo-classes use one (:hover). The double colon was introduced in CSS3 specifically to tell them apart. You’ll still see old code using a single colon for pseudo-elements (:before) — browsers accept it for backwards compatibility — but in any new code you write, use the double colon. It signals your intent clearly: “this targets a generated part of the element.”
If you’re still warming up on how selectors work in general, it’s worth circling back to CSS selectors first — pseudo-elements build directly on that foundation.
The big two: ::before and ::after
By far the most-used pseudo-elements are ::before and ::after. They let you insert generated content before or after an element’s actual content. Picture them as two invisible mini-elements living just inside your element, one at the very start and one at the very end.
Here’s the catch that trips everyone up at first: for ::before and ::after to show up at all, you must give them a content property. Without it, nothing appears.
.note::before {
content: "💡 ";
}
<p class="note">Save your work often.</p>
That renders as 💡 Save your work often. — the lightbulb was never in the HTML; CSS conjured it. The content value can be plain text, an emoji, an empty string, or even nothing-but-a-shape (more on that shortly).
No content, no pseudo-element
This is the number-one beginner mistake. If you set content: ""; you get an empty but present box you can size and style. If you omit content entirely, the pseudo-element is not generated at all — your background, border, and width will do absolutely nothing because there’s no box to apply them to. Always include content, even if it’s just content: "";.
Generated content is “real” content
Whatever you put in content shows up on the page like normal text. That means a content: "→" actually places an arrow character that’s visible, copyable in some cases, and styleable with color, font-size, and the rest.
.external::after {
content: " ↗";
color: #00B8E6;
}
<a href="https://example.com" class="external">Visit ACY Partner Indonesia</a>
Now every link with the external class gets a small cyan arrow tacked on after the text, hinting that it opens elsewhere — a tidy UI touch with zero extra markup.
Making decorative shapes with empty content
Here’s where ::before and ::after get genuinely powerful. Set content: ""; (empty) and then give the pseudo-element a width, height, background, or border — and you’ve created a pure decoration: a divider, a dot, a custom underline, a little badge.
.title::after {
content: "";
display: block;
width: 60px;
height: 4px;
background: #00B8E6;
margin-top: 8px;
}
<h2 class="title">Our Services</h2>
That paints a short accent bar underneath the heading. The pseudo-element holds no text at all — its entire job is to be a styled box. This pattern shows up everywhere: section underlines, card ribbons, checkmarks, custom bullets, speech-bubble tails.
Pseudo-elements are inline by default
A fresh ::before or ::after behaves like an inline element, so width and height are ignored until you change that. Add display: block (or inline-block) to make it respect dimensions. This is why the example above sets display: block before giving it a width and height. If your decorative box “won’t size,” this is almost always why. It also helps to know how the display property changes an element’s box behavior.
A practical example: custom list bullets
Default list bullets are limited and hard to style. With ::before you can replace them with anything — an arrow, a checkmark, a colored dot of exactly the size you want:
.checklist {
list-style: none; /* remove the default bullets */
padding-left: 0;
}
.checklist li::before {
content: "✓ ";
color: #16a34a;
font-weight: bold;
}
<ul class="checklist">
<li>Mobile friendly</li>
<li>Fast loading</li>
<li>SEO optimized</li>
</ul>
You strip the native bullets with list-style: none, then prepend your own green checkmark via ::before. Now you control the exact symbol, color, and spacing — something the built-in bullets never let you do.
Typographic pseudo-elements
A handful of pseudo-elements target text specifically. These don’t need a content property — the text they style already exists.
::first-letter and ::first-line
::first-letter styles just the opening letter of a block — perfect for the classic “drop cap” you see at the start of magazine articles. ::first-line styles the entire first line, however long it happens to be.
.article p::first-letter {
font-size: 3em;
font-weight: bold;
float: left;
line-height: 1;
margin-right: 6px;
color: #00B8E6;
}
The first letter of each paragraph balloons into a large, floated drop cap while the rest of the text wraps neatly around it. Because ::first-line reflows with the text, its boundary shifts as the window resizes — the browser recalculates what “the first line” is on the fly.
::selection
::selection styles the part of the page the user has highlighted with their cursor. It’s a small detail that makes a site feel polished and on-brand:
::selection {
background: #00B8E6;
color: #ffffff;
}
Now whenever someone drags to select text, the highlight uses your brand cyan instead of the browser default. Only a few properties work here (color, background, text-decoration, and a couple of others), but that’s plenty for a clean highlight.
::placeholder
::placeholder styles the faint hint text inside an empty <input> or <textarea>. By default it’s a flat gray; this lets you match it to your design:
input::placeholder {
color: #94a3b8;
font-style: italic;
}
<input type="email" placeholder="you@example.com">
It’s a tiny touch, but consistent placeholder styling is one of those things that quietly separates a careful interface from a sloppy one.
Positioning pseudo-elements precisely
For more advanced decorations — tooltips, badges, ribbons, corner flags — you’ll often want to place a pseudo-element at an exact spot. The standard recipe is to make the parent position: relative and the pseudo-element position: absolute:
.badge {
position: relative;
}
.badge::after {
content: "NEW";
position: absolute;
top: -10px;
right: -10px;
background: #ef4444;
color: #fff;
font-size: 12px;
padding: 2px 6px;
border-radius: 4px;
}
<button class="badge">Pricing</button>
That pins a little red “NEW” tag to the top-right corner of the button, floating just outside its edge. The position: relative on the parent gives the absolutely-positioned pseudo-element something to anchor to. If positioning feels shaky, a quick refresher on CSS positioning will make this pattern much clearer.
A few gotchas worth knowing
A handful of rules will save you debugging time:
- They only work on elements that have content. You can’t add
::before/::afterto “empty” replaced elements like<img>,<input>,<br>, or<hr>. These elements don’t have a content area for the pseudo-element to live in, so it simply won’t render. - Generated content isn’t selectable like normal text in many browsers, and screen readers may or may not announce it. Don’t put important information — text the user genuinely needs to read — inside
content. Keep it decorative. - You can have one
::beforeand one::afterper element, not several of each. Two decorations is the limit per element; for more, you’ll nest extra elements. contentcan pull in an attribute’s value usingattr(), which is genuinely handy:
a::after {
content: " (" attr(href) ")";
color: #64748b;
}
That appends each link’s URL in parentheses right after the link text — useful for print stylesheets where the reader can’t click.
Don't hide essential content in a pseudo-element
Because generated content can be skipped by assistive technology and isn’t part of the real document, never use ::before/::after for content that must be read — labels, prices, error messages, button text. Use them for decoration and visual flourishes only. If the information matters, put it in your HTML where everyone, including screen readers and search engines, can reach it.
Wrapping up
Pseudo-elements let you style and even invent parts of an element that don’t exist as real tags — a clean way to add decoration without cluttering your HTML. Here’s the core to carry forward:
- Pseudo-elements use a double colon (
::before), unlike pseudo-classes’ single colon (:hover). ::beforeand::afterinsert generated content and always need acontentproperty — evencontent: ""for a pure-decoration box.- Set
display: blockon::before/::aftersowidthandheighttake effect for shapes like underlines and dividers. ::first-letter,::first-line,::selection, and::placeholderstyle text-level pieces with nocontentneeded.- Pair
position: relativeon the parent withposition: absoluteon the pseudo-element to place badges and ribbons precisely. - Keep generated content decorative — never hide essential, must-read information inside it.
Master ::before and ::after and you’ll spot dozens of places they simplify your markup. They pair especially well with the styling you’ve learned so far — and once you start combining them with transitions and transforms, you’ll have everything you need to build polished, animated UI details from CSS alone.