CSS Pseudo-Elements Mastery: ::before, ::after, Counters & Decorative Techniques
Master CSS pseudo-elements for decorative, semantic design. Covers ::before/::after, the content property, decorative lines, CSS-only tooltips, drop caps, ::selection, ::marker, CSS counters, pure CSS icons, overlay effects, and best practices.
- 1. CSS Flexbox Mastery: The Complete Visual Guide to Flexible Layouts
- 2. CSS Grid Mastery: From Basics to Advanced Layouts
- 3. CSS Custom Properties (Variables) Mastery: Dynamic Theming & Beyond
- 4. CSS Animations & Transitions Mastery: From Hover Effects to Keyframe Sequences
- 5. CSS Selectors Deep Dive: From Basic to :has() & :is()
- 6. CSS Box Model Mastery: Content, Padding, Border & Margin Explained
- 7. CSS Responsive Design Mastery: Mobile-First, clamp(), & Container Queries
- 8. CSS Specificity & Cascade Mastery: How Browsers Resolve Conflicts
- 9. CSS Modern Layout Techniques: Scroll Snap, Subgrid, Logical Properties & More
- 10. CSS Pseudo-Elements Mastery: ::before, ::after, Counters & Decorative Techniques
Pseudo-elements let you style parts of elements that don't exist in the HTML. They're virtual elements created entirely in CSS — perfect for decorative touches, icons, tooltips, and visual effects that keep your HTML clean and semantic.
Think of pseudo-elements as CSS's way of adding visual accessories without cluttering your markup. In this guide, we'll master every pseudo-element and build practical patterns you'll use in real projects.
1. What Are Pseudo-Elements?
Pseudo-elements target specific parts of an element's rendering. They use double colons (::) to distinguish them from pseudo-classes (:hover, :focus).
| Pseudo-element | What It Targets |
|---|---|
::before | Inserts generated content before element's content |
::after | Inserts generated content after element's content |
::first-line | The first rendered line of text |
::first-letter | The first letter of the first line |
::placeholder | Placeholder text in form inputs |
::selection | Text selected/highlighted by the user |
::marker | Bullet/number of list items |
::backdrop | Background behind dialogs/fullscreen elements |
Historical Note: Single colon (
:before) works for backward compatibility, but double colon (::before) is the correct modern syntax.
2. ::before and ::after — The Power Duo
These are the most versatile pseudo-elements. They create child elements at the start or end of an element's content:
.label::before {
content: "★ "; /* Required! */
color: #f59e0b;
}
.label::after {
content: " →";
color: #3b82f6;
}
The content Property Rules
::before and ::after require the content property. Without it, the pseudo-element doesn't exist.
| Value | Result |
|---|---|
content: "" | Empty (used for decorative shapes) |
content: "text" | Inserts literal text |
content: attr(data-label) | Inserts the value of an HTML attribute |
content: counter(name) | Inserts a CSS counter value |
content: url(icon.svg) | Inserts an image |
content: open-quote | Inserts the opening quote character |
3. Decorative Lines and Dividers
One of the most common uses of ::before / ::after is creating decorative elements.
Heading with Line
.fancy-heading {
display: flex;
align-items: center;
gap: 1rem;
}
.fancy-heading::before,
.fancy-heading::after {
content: "";
flex: 1;
height: 2px;
background: linear-gradient(to right, transparent, #cbd5e1, transparent);
}
Accent Bar on Cards
.card::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(to right, #3b82f6, #8b5cf6);
border-radius: 4px 4px 0 0;
}
4. Tooltips with Pseudo-Elements
Create CSS-only tooltips using ::before and ::after:
.tooltip {
position: relative;
}
/* Tooltip text */
.tooltip::after {
content: attr(data-tooltip);
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
padding: 0.5rem 0.75rem;
background: #1e293b;
color: white;
font-size: 0.75rem;
border-radius: 0.375rem;
white-space: nowrap;
opacity: 0;
pointer-events: none;
transition: opacity 0.2s;
}
/* Arrow */
.tooltip::before {
content: "";
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
border: 6px solid transparent;
border-top-color: #1e293b;
opacity: 0;
pointer-events: none;
transition: opacity 0.2s;
}
.tooltip:hover::after,
.tooltip:hover::before {
opacity: 1;
}
5. ::first-line and ::first-letter
Drop Cap Effect
.article p:first-of-type::first-letter {
font-size: 3.5em;
float: left;
line-height: 0.8;
padding-right: 0.1em;
font-weight: 700;
color: #3b82f6;
}
Styling the First Line
.article p::first-line {
font-weight: 600;
color: #1e293b;
font-size: 1.1em;
}
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.
6. ::placeholder Styling
Customize the look of placeholder text in form inputs:
input::placeholder {
color: #94a3b8;
font-style: italic;
font-size: 0.875rem;
}
/* Focused state — hide placeholder */
input:focus::placeholder {
opacity: 0;
transition: opacity 0.2s;
}
7. ::selection — Custom Text Highlight
Change how selected text looks:
::selection {
background: #3b82f6;
color: white;
}
/* Per-element selection */
.warning::selection {
background: #fbbf24;
color: #1e293b;
}
Select this text to see a custom purple highlight! The ::selection pseudo-element lets you customize the background color and text color of user-selected text. This is a subtle but premium design touch.
8. ::marker — Custom List Bullets
Style the bullets or numbers of list items:
li::marker {
color: #3b82f6;
font-weight: 700;
font-size: 1.2em;
}
/* Ordered lists */
ol li::marker {
color: #8b5cf6;
content: counter(list-item) ". ";
}
- Styled bullet color
- Bold and larger
- Clean and simple
- Custom emoji bullets
- Using content property
- Simple and effective
9. CSS Counters with Pseudo-Elements
Create custom numbering systems without JavaScript:
.steps {
counter-reset: step-counter;
}
.step {
counter-increment: step-counter;
}
.step::before {
content: "Step " counter(step-counter);
display: inline-block;
background: #3b82f6;
color: white;
padding: 0.25rem 0.75rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 700;
margin-bottom: 0.5rem;
}
10. Shapes and Icons with Pseudo-Elements
Create pure CSS shapes without any images:
CSS Triangle
.arrow-down::after {
content: "";
display: inline-block;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-top: 8px solid currentColor;
}
Hamburger Menu Icon
.hamburger {
width: 24px;
height: 2px;
background: currentColor;
position: relative;
}
.hamburger::before,
.hamburger::after {
content: "";
position: absolute;
width: 100%;
height: 2px;
background: currentColor;
left: 0;
}
.hamburger::before { top: -7px; }
.hamburger::after { top: 7px; }
Close (X) Icon
.close::before,
.close::after {
content: "";
position: absolute;
width: 20px;
height: 2px;
background: currentColor;
top: 50%;
left: 50%;
}
.close::before { transform: translate(-50%, -50%) rotate(45deg); }
.close::after { transform: translate(-50%, -50%) rotate(-45deg); }
The CSS triangle trick works by making an element with zero width/height, then using thick borders. Since borders meet at 45° angles, making 3 sides transparent creates a triangle shape.
11. Overlay and Gradient Effects
Dark Overlay on Images
.hero {
position: relative;
}
.hero::after {
content: "";
position: absolute;
inset: 0; /* top: 0; right: 0; bottom: 0; left: 0 */
background: linear-gradient(to bottom, transparent 50%, rgba(0, 0, 0, 0.7));
pointer-events: none; /* Allow clicks to pass through */
}
Gradient Border Effect
.gradient-border {
position: relative;
padding: 3px; /* The "border" width */
background: linear-gradient(135deg, #3b82f6, #8b5cf6, #ec4899);
border-radius: 0.5rem;
}
.gradient-border > * {
background: white;
border-radius: calc(0.5rem - 3px);
}
12. Common Mistakes & Gotchas
Mistake 1: Forgetting content: ""
/* ❌ Won't appear */
.box::before {
width: 50px;
height: 50px;
background: blue;
}
/* ✅ Works */
.box::before {
content: ""; /* Required! */
width: 50px;
height: 50px;
background: blue;
display: block;
}
Mistake 2: Pseudo-Elements on Self-Closing Tags
::before and ::after don't work on elements that can't have children:
/* ❌ Won't work */
img::before { }
input::before { }
br::after { }
/* ✅ These work because they can have children */
div::before { }
span::after { }
a::before { }
Mistake 3: Content in ::before/::after for Screen Readers
Content added via content property is read by some screen readers. For purely decorative pseudo-elements, this can be confusing. Use aria-hidden on the parent or ensure the content doesn't convey important information.
Mistake 4: Not Using display: block on Empty Pseudo-Elements
Pseudo-elements are inline by default. If you're creating a decorative box:
/* ❌ Won't have dimensions */
.box::before {
content: "";
width: 100px;
height: 100px;
}
/* ✅ display: block makes width/height work */
.box::before {
content: "";
display: block;
width: 100px;
height: 100px;
}
Conclusion
Pseudo-elements are CSS's secret weapon for clean, semantic design:
::before/::after— Add decorative content, shapes, overlays, tooltips::first-letter— Drop caps and typographic flair::first-line— Style the opening line of text::placeholder— Custom form input placeholders::selection— Custom text highlight colors::marker— Custom list bullets and numbering- CSS Counters — Automatic numbering without JavaScript
- Pure CSS Icons — Triangles, hamburgers, close buttons
The golden rule: if something is purely decorative, it belongs in pseudo-elements. Keep your HTML for semantic content and let CSS handle the visual extras.
🧠 Test Your Knowledge
Now that you've learned the concepts, let's see if you can apply them! Take this quick quiz to test your understanding.