Problem Solved

CSS · pseudo-elements

Home › Courses › CSS › Problems › Pseudo-elements
🎨 Concept · 3 min read

Pseudo-elements.
::before, ::after, and content.

A pseudo-element is a part of an element that CSS creates virtually — it doesn't exist in the HTML. ::before and ::after are the most used: they add content inside an element without touching the markup. A quote mark, an arrow, a checkmark, a decorative shape — all without a single HTML element. This page covers the five pseudo-elements, the content property that makes them work, and the patterns that actually help.

Short answer: Pseudo-elements style virtual parts of an element. ::before and ::after create content inside an element — they need content to render. ::first-line, ::first-letter, ::selection style existing parts. Use double colons (::before) to distinguish from pseudo-classes (:hover). Use pseudo-elements for decoration only — icons, quote marks, arrows. For meaningful content, use real HTML. Screen readers may or may not announce pseudo-element content.

✅ The five pseudo-elements

Each one styles a different virtual part.

::before

Content inserted before an element's children. Needs content. Uses for icons, quote marks, decorations.

::after

Content inserted after an element's children. Same rules — needs content.

::first-line

Styles the first line of text in a block. Affected by width and line breaks.

::first-letter

Styles the first letter of a block. For drop caps in articles.

::selection

Styles text the user has selected with the cursor. Limited properties (color, background).

✅ Case 1 — Blockquotes

Quote mark without an HTML element.

quote.css
.quote {
    font-style: italic;
    padding-left: 1.5rem;
    position: relative;
    color: #334155;
}

.quote::before {
    content: '"';
    position: absolute;
    left: 0;
    top: -0.5rem;
    font-size: 3rem;
    line-height: 1;
    color: #2965f1;
    font-family: Georgia, serif;
}
HTML:
<p class="quote">The best way to predict the future is to invent it.</p>

Rendered:
┌─────────────────────────────┐
│ " The best way to predict   │
│ the future is to            │
│ invent it.                  │
└─────────────────────────────┘

✅ Quote mark is decorative — pure CSS.
✅ No HTML needed.
✅ Changes the quote style by changing one CSS rule.
✅ Screen readers skip the decorative quote.

⚠️ Use <blockquote> for semantic meaning. ::before is only for visual flourish.

✅ Common use cases

Where pseudo-elements earn their keep.

use-cases.css
/* 1. External link icon */
a[href^="http"]::after {
    content: ' ↗';
    font-size: 0.75em;
    color: #64748b;
}

/* 2. Required field asterisk */
label.required::after {
    content: ' *';
    color: red;
}

/* 3. Decorative underline on hover */
.link::after {
    content: '';
    display: block;
    width: 0;
    height: 2px;
    background: #2965f1;
    transition: width 0.2s;
}
.link:hover::after {
    width: 100%;
}

/* 4. Selection highlight */
::selection {
    background: #2965f1;
    color: white;
}

/* 5. Drop cap */
.article > p:first-of-type::first-letter {
    font-size: 3.5rem;
    font-weight: bold;
    float: left;
    line-height: 0.9;
    padding-right: 0.1em;
    color: #2965f1;
}
1. External links get a ↗ icon automatically.
2. Required labels get a red asterisk.
3. Hover underline animates from 0 to 100% width.
4. Selection color matches the brand.
5. First letter of first paragraph is enlarged as a drop cap.

All five use cases: decorative, no HTML changes needed.
For icons that carry meaning, use real HTML with aria-label or alt text.

✅ The content property

What makes ::before and ::after render.

content-property.css
/* Text */
.badge::before {
    content: '★ ';
}

/* Empty string — for shapes and borders */
.shape::before {
    content: '';
    display: block;
    width: 20px;
    height: 20px;
    background: #2965f1;
    border-radius: 50%;
}

/* Special characters */
.breadcrumb a + a::before {
    content: ' / ';
    color: #94a3b8;
}

/* Attribute values (great for printing) */
@media print {
    a[href]::after {
        content: ' (' attr(href) ')';
        font-size: 0.8em;
    }
}

/* Counter (numbered lists without <ol>) */
.section-title {
    counter-increment: section;
}
.section-title::before {
    content: counter(section) '. ';
    color: #2965f1;
    font-weight: bold;
}
content takes:
✅ A string: content: '→';
✅ An empty string: content: '';
✅ An attribute: content: attr(href);
✅ A counter: content: counter(section);
✅ An image: content: url(icon.svg);

But NOT:
❌ HTML elements -> content: '<b>'; // renders as string literals
❌ JavaScript -> content: runCode();
❌ Dynamic input parameters

The content property is for static or CSS-generated layout adjustments only.

Don't use ::before and ::after for content that needs to be accessible. Screen reader support for pseudo-element content is inconsistent — some announce it, some skip it entirely. If the content carries meaning — an error icon, a warning, a count, a status — it must be in the HTML. Use pseudo-elements for decoration only: quote marks, arrows, dividers, decorative shapes. If a sighted user needs to know it, a screen reader user needs to know it too — and CSS-generated content doesn't reliably reach the accessibility tree. For meaningful icons, use <svg aria-label="..."> or a real <span> with visually-hidden text.

⚠️ Common mistakes

The pseudo-element mistakes that cost real time.

1. Forgetting content

No content means no render. Fix: content: '' for shapes, content: 'text' for text.

2. Using ::before on a self-closing element

<img>, <input> — no children. Fix: wrap in a div and style that.

3. Using single-colon syntax

:before works but is legacy. Fix: ::before (double colon).

4. Pseudo-elements for meaningful content

Screen readers may skip it. Fix: use real HTML for meaning; CSS for decoration.

5. Not setting display on the pseudo-element

Defaults to inline — width/height don't apply. Fix: display: block or inline-block.

6. Position not set on parent

::before with position: absolute needs a positioned parent. Fix: position: relative on the parent.

7. Trying to use ::before on replaced elements

<video>, <canvas>, <iframe> — no. Fix: wrap.

8. Duplicating content between HTML and CSS

Screen reader announces twice or not at all. Fix: pick one — usually HTML.

❓ Frequently asked

What are pseudo-elements in CSS?

Pseudo-elements let you style a specific part of an element that isn't a real DOM node. The five main ones: ::before and ::after create virtual content inside an element; ::first-line and ::first-letter style the first line or first letter; ::selection styles the highlighted text. Pseudo-elements use double colons to distinguish them from pseudo-classes. ::before and ::after are the most used — they create decorative content without adding HTML.

What is the content property?

The content property is what makes ::before and ::after show anything. It takes a string, an image URL, a counter, or a few special keywords. Syntax: .quote::before { content: '"'; }. Without content, ::before and ::after don't render. Use content: '' for purely decorative shapes. Common values: content: '"' for quote marks, content: counter(step) for numbered lists.

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

A pseudo-class targets an element in a specific state — :hover, :focus, :first-child. A pseudo-element targets a specific part of an element — ::before, ::after, ::first-line, ::first-letter, ::selection. The syntax is the giveaway: pseudo-classes use one colon (:hover), pseudo-elements use two (::before). Different concepts, different syntax.

Can I use pseudo-elements for accessibility?

Carefully. Content added by ::before and ::after is not always announced by screen readers — some do, some don't. If the content conveys information, it should be in the HTML. Use pseudo-elements for decoration — visual flourishes, borders, shapes — not for meaningful text. If you must use ::before to add text, verify with a screen reader.

Why doesn't ::before/::after show up?

Four common causes. (1) No content property. (2) The parent is display: inline. (3) The parent has no content. (4) Z-index or overflow issue. Fix: add content: '', use display: inline-block on the parent, ensure the parent has content, and check position/z-index/overflow.

When should I use ::before vs adding an element?

Use ::before/::after when the content is purely decorative and doesn't need to be in the DOM. Examples: a quote mark before a blockquote, a decorative arrow, a colored line under a heading. Use a real element when the content is semantic — an icon that conveys meaning, a badge with a number. Rule of thumb: if a screen reader user needs to know it's there, use HTML. If it's visual flourish only, ::before/::after is fine.

🔗 Related

← All CSS problems
Problem solved · CSS