CSS Pseudo-Elements
Learn how CSS pseudo-elements ::before, ::after, ::first-letter,::placeholder, ::selection, and ::marker work, with live visual examples and code you can try right away.
If you followed along with the last lesson on pseudo-classes, you already have a solid feel for how CSS can respond to element states and positions: :hover, :focus, :nth-child(), and their friends. That was all about targeting elements based on when or where they appear in the document.
Today we’re going one step further, and honestly, this one is more enjoyable to work with.
CSS pseudo-elements let you target a specific part of an element, not the whole thing. The first letter of a paragraph. The text area right before the element’s content. The color of highlighted text when a user selects it. All of that, styled with pure CSS, without touching your HTML at all.
That’s the real win here: you’re adding visual detail through CSS alone. No extra <span> tags. No wrapper divs. Just a selector and a few properties.
01Pseudo-Class vs Pseudo-Element: Let’s Clear This Up First
Both look similar in code, and beginners often mix them up. Here’s the simplest way to tell them apart:
Targets an element based on its state or position.
Examples:
:hover, :focus, :nth-child(2)
Targets a specific part of an element.
Examples:
::before, ::first-letter, ::selection
A pseudo-class says: “when this element is in a certain condition, style it.” A pseudo-element says: “take this specific chunk of or around the element, and style just that.”
The selectors we covered in pseudo-classes all target whole elements. The selectors we’re covering today reach into or around elements in a way that pseudo-classes simply can’t.
02The Double Colon and Where It Came From
You’ll notice pseudo-elements use a double colon (::), while pseudo-classes use a single colon (:). This distinction wasn’t always there.
In CSS2, both used a single colon. So you’d write :before and :after alongside :hover and :focus, all looking the same. CSS3 introduced the double colon specifically for pseudo-elements, to visually separate them from pseudo-classes in your code.
So :before became ::before, :first-letter became ::first-letter, and so on. The old single-colon versions still work in most browsers for backward compatibility, but the correct, modern syntax is always the double colon. Use that.
03::before and ::after: The Two You’ll Reach For Most
Of all the CSS pseudo-elements, these two are the most powerful and the ones you’ll use constantly in real projects. Let’s give them proper attention.
::before inserts a virtual element right before an element’s actual content. ::after inserts one right after it. Neither of them exists in your HTML. They’re created entirely by CSS, which is what makes them so useful for decorative effects without cluttering your markup.
The content Property: This One Is Non-Negotiable
Here’s something that trips up almost every beginner at least once: ::before and ::after require a content property. Without it, the pseudo-element won’t appear at all, even if you give it a width, height, and background color. The browser just ignores it.
The value of content can be a few different things:
/* Empty string: pseudo-element exists, but shows no text
You'd use this when you want to size/position it as a shape */
.box::before {
content: "";
display: block;
width: 40px;
height: 4px;
background: #6366f1;
}
/* A text string */
.required-field::after {
content: " *";
color: #ff5c7a;
}
/* A symbol or Unicode character */
.arrow-link::after {
content: " →";
}
/* Pulled dynamically from an HTML attribute */
[data-tooltip]::before {
content: attr(data-tooltip);
}
The content: "" with an empty string is probably the most common pattern you’ll see. You’re not trying to show text. You just need the pseudo-element to exist so you can position and style it as a shape, a line, an overlay, whatever you need.
Positioning and Display: Getting Them to Behave
By default, ::before and ::after are inline. That means they flow with text. Most of the time, you’ll want to change their display type:
/* Block: takes up its own line, can have width/height */
.card::before {
content: "";
display: block;
width: 100%;
height: 3px;
background: linear-gradient(90deg, #6366f1, #ec4899);
}
/* Absolute: positioned relative to the parent element */
.card {
position: relative; /* Required when using position: absolute inside */
}
.card::after {
content: "";
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 50%;
background: linear-gradient(to top, rgba(0, 0, 0, 0.6), transparent);
pointer-events: none;
}
/* Flex: useful for centering content inside the pseudo-element */
.badge::after {
content: "New";
display: flex;
align-items: center;
justify-content: center;
}
position: absolute on a ::before or ::after, the parent element must have position: relative (or absolute, fixed, or sticky). If you forget this, the pseudo-element will position itself relative to a different ancestor, usually not the result you want.Four Real Uses You’ll Actually Build
1. A decorative section heading with lines on both sides
Featured Projects
h2 {
display: flex;
align-items: center;
gap: 1rem;
}
h2::before,
h2::after {
content: "";
flex: 1;
height: 2px;
border-radius: 2px;
}
h2::before {
background: linear-gradient(90deg, transparent, #6366f1);
}
h2::after {
background: linear-gradient(90deg, #6366f1, transparent);
}
No extra HTML. Just a clean CSS trick using ::before and ::after to add lines that automatically fill the remaining space on either side of the heading text.
2. A notification count badge on a button
.notification-btn {
position: relative;
}
.notification-btn::after {
content: "3";
position: absolute;
top: -7px;
right: -7px;
width: 22px;
height: 22px;
background: #ec4899;
color: #fff;
border-radius: 50%;
font-size: 0.68rem;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
}
3. A large opening quote mark before a blockquote
The details are not the details. They make the design. And that, in the end, is what separates the truly great from the merely good.
blockquote {
position: relative;
padding: 1.5rem 1.5rem 1.5rem 3rem;
}
blockquote::before {
content: "\201C"; /* Unicode for the opening quotation mark */
position: absolute;
top: -0.3rem;
left: 0.65rem;
font-size: 4.5rem;
color: #6366f1;
font-family: Georgia, serif;
line-height: 1;
opacity: 0.75;
}
4. A gradient glow overlay behind a card
.glow-card {
position: relative;
overflow: hidden;
}
.glow-card::before {
content: "";
position: absolute;
top: -60%;
left: -30%;
width: 160%;
height: 160%;
background: radial-gradient(
ellipse at center,
rgba(99, 102, 241, 0.2) 0%,
transparent 65%
);
pointer-events: none; /* Doesn't block clicks */
}
::before and ::after do not work on self-closing HTML elements like <img>, <input>, <br>, or <hr>. These elements can’t have children, and since pseudo-elements are essentially CSS children, the browser ignores them. Use a wrapper element in those cases.04CSS ::first-letter: That Drop Cap You See in Print
Open a well-designed magazine or a classic novel chapter and you’ll often spot the first letter styled much larger, sometimes in a different font or color. That’s called a “drop cap,” and ::first-letter is the CSS pseudo-element that makes it happen.
Designing a good interface isn’t just about picking the right colors or fonts. It’s about making decisions that serve the person on the other side of the screen. Every detail you add either helps or gets in the way. The best designs are the ones that don’t draw attention to themselves.
p::first-letter {
float: left;
font-size: 3.6rem;
font-weight: 700;
line-height: 0.82;
margin-right: 0.12em;
margin-top: 0.06em;
color: #6366f1;
font-family: Georgia, serif;
}
A few things worth keeping in mind:
::first-letteronly works on block-level elements like<p>,<div>,<h1>, and similar. It won’t work on inline elements.- If the paragraph starts with a punctuation character (like a quote mark
"), some browsers will include that as part of the first-letter selection. - You can use
float,font-size,color,font-family,line-height,padding,margin, andbackgroundon it.
It’s a great way to give blog posts and articles an editorial feel without any additional markup.
05CSS ::first-line: Different Styling for Your Opening Line
::first-line targets the first rendered line of a block of text, as it actually appears on screen. What makes this interesting is that the “first line” is dynamic: it depends on the width of the container and the viewport. As the page resizes, what counts as the first line changes. CSS handles that automatically.
The constraint of having only one line to make an impression is actually a useful design challenge. It forces you to lead with what matters most. Everything else can follow, but the first line sets the tone for everything that comes after it.
p::first-line {
color: #a5b4fc;
font-weight: 700;
letter-spacing: 0.025em;
}
::first-line doesn’t support every CSS property. Stick to text-related ones and you’ll be fine. The properties it supports include:
colorfont-*properties (font-size, font-weight, font-family, etc.)text-decorationandtext-transformletter-spacingandword-spacingline-heightbackground
Properties like margin, padding, border, and width won’t have any effect here. Keep it to text styling and you’re good.
06Custom Placeholder CSS Styling with ::placeholder
Every form input has a placeholder attribute, that hint text you see before someone starts typing. Browsers style it however they feel like by default, usually a muted gray that may not fit your design at all.
The ::placeholder pseudo-element gives you control over it.
/* Default: browser handles it */
input::placeholder {
color: #4b5563;
}
/* Styled: take full control */
input::placeholder {
color: #818cf8;
font-style: italic;
font-weight: 500;
opacity: 1; /* Crucial: overrides browser's default opacity dimming */
}
opacity: 1 line is easy to miss but important. Firefox reduces placeholder opacity to around 0.54 by default. Without opacity: 1, your custom color will appear dimmer than you expect. Set it explicitly and your color shows up exactly as defined.You can adjust color, font-style, font-size, letter-spacing, and a handful of other text properties. Layout properties like width or padding won’t work here since the placeholder text sits inside the input’s own space.
07Changing Your Selection Highlight Color with ::selection
When a user drags their cursor across text, the browser applies a default highlight color, typically blue. With ::selection, you can replace that with any color you want. It’s a small detail, but it makes a real impression when it matches your brand.
☝️ Click and drag to select the text above
/* Apply to all selected text on the page */
::selection {
background-color: #ec4899;
color: #ffffff;
}
/* Or target just a specific element */
.article-body::selection {
background-color: #6366f1;
color: #fff;
}
color and background-color work reliably inside ::selection. Properties like text-shadow, border, and font-weight are ignored by most browsers. Keep it to just those two.Also worth noting: ::selection doesn’t need a content property because it’s not generating any new content. It’s restyling something that already exists.
08CSS ::marker: Clean List Bullet Styling, Finally
Before ::marker was well-supported, customizing list bullets meant removing the default bullet entirely (list-style: none), making each <li> position-relative, and adding a ::before with a custom character. It worked, but it was a workaround.
Now, with ::marker, you can style the actual bullet directly:
- Typography
- Color Palette
- Grid Layout
- Typography
- Color Palette
- Grid Layout
- Typography
- Color Palette
- Grid Layout
/* Styling bullets with ::marker */
ul li::marker {
color: #ec4899;
font-weight: 700;
font-size: 1.15em;
}
/* Styling ordered list numbers */
ol li::marker {
color: #6366f1;
font-weight: 700;
}
/* Even swap the bullet character entirely */
ul li::marker {
content: "✓ ";
color: #2ecc71;
}
For more creative custom markers (like arrows, icons, or complex shapes), the ::before approach is still widely used:
ul {
list-style: none;
padding-left: 1.5rem;
}
ul li {
position: relative;
}
ul li::before {
content: "→";
position: absolute;
left: -1.3rem;
color: #6366f1;
font-weight: 700;
}
::marker supports color, font-size, font-weight, content, and a few others. It’s not the most flexible selector yet, but for straightforward bullet color and size changes, it’s the cleanest option available.
09The Mistakes That Catch Beginners Off Guard
If you add a ::before or ::after and nothing shows up in the browser, this is always the first thing to check. Even content: "" is required. Without it, the pseudo-element simply doesn’t render.
When you want a pseudo-element positioned inside a specific element, that element needs position: relative. Without it, the pseudo-element will anchor itself to whatever the nearest positioned ancestor is, which is often the viewport itself.
Self-closing elements like <img>, <input>, and <hr> can’t have children. Since pseudo-elements are treated as CSS-generated children, they simply don’t work on these elements. Wrap them in a <div> or <span> if you need that effect.
::first-line only supports a limited set of text-related properties. ::selection reliably supports only color and background-color. Other properties are silently ignored by the browser with no error message, which can make this confusing to debug.
Since ::before and ::after aren’t real DOM elements, document.querySelector("::before") will return nothing. To change their styles dynamically, modify CSS custom properties or class names on the parent element instead. Don’t worry if JavaScript sounds unfamiliar right now, we’ll cover that separately later in the series.
10Quick Reference: All Seven Pseudo-Elements at a Glance
| Pseudo-Element | What It Targets | Needs content? | Works on |
|---|---|---|---|
| ::before | Virtual element before the element’s content | Yes | Block & inline elements (not self-closing) |
| ::after | Virtual element after the element’s content | Yes | Block & inline elements (not self-closing) |
| ::first-letter | The very first letter (or character) of text | No | Block-level elements only |
| ::first-line | The first rendered line of a text block | No | Block-level elements only |
| ::placeholder | The placeholder text inside form inputs | No | <input> and <textarea> |
| ::selection | Text that the user has highlighted | No | Most elements (limited properties) |
| ::marker | The bullet or number of a list item | No | <li> and display: list-item elements |
::backdrop (used for fullscreen overlays), ::cue (for WebVTT captions), and ::part() (for styling inside web components). These are more advanced and niche. The seven covered here are the ones you’ll encounter and use in everyday projects.11A Quick Full Example Putting Several Together
Let’s see what it looks like when you combine a few of these in one small component:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Pseudo-Elements Demo</title>
<style>
body {
background: #141d34;
color: #cbd5e1;
font-family: system-ui, sans-serif;
padding: 2rem;
}
/* Drop cap on the first paragraph */
.post-body p:first-of-type::first-letter {
float: left;
font-size: 3.5rem;
font-weight: 700;
line-height: 0.82;
margin-right: 0.1em;
color: #6366f1;
}
/* Pink highlight when selecting text */
.post-body::selection {
background: #ec4899;
color: #fff;
}
/* Tag badge using ::before */
.tag {
position: relative;
padding-left: 1.1rem;
font-size: 0.8rem;
color: #a5b4fc;
}
.tag::before {
content: "#";
position: absolute;
left: 0;
color: #6366f1;
font-weight: 700;
}
/* Styled list markers */
ul li::marker {
color: #ec4899;
}
/* Styled placeholder */
input::placeholder {
color: #818cf8;
font-style: italic;
opacity: 1;
}
</style>
</head>
<body>
<article class="post-body">
<p>
Pseudo-elements give you a layer of styling control that most beginners
don't know exists. Once you start using them, you'll notice them everywhere.
</p>
<ul>
<li>No extra HTML required</li>
<li>Pure CSS decorative effects</li>
<li>Works with the cascade and inheritance</li>
</ul>
<p><span class="tag">css</span> <span class="tag">frontend</span></p>
</article>
<input type="text" placeholder="Search articles..." />
</body>
</html>
Notice how each pseudo-element serves a specific purpose, none of them are doing the same job, and the HTML stays clean throughout. That’s the right way to think about it.
Pseudo-elements are one of those CSS features that quietly show up everywhere once you know what to look for. That heading with decorative lines on both sides? ::before and ::after. That big first letter at the start of a chapter? ::first-letter. The custom text highlight on a portfolio site? ::selection.
Go back through the examples here and try building each one from scratch. The ::before and ::after demos especially, since those are the ones you’ll reach for most often. Experiment with positioning, play with the content values, try combining two or three in a single component.
CSS Pseudo-Elements
Loading challenge…
Ready to Practice?
Select a difficulty level above to load your challenge.