CSS

CSS ::before and ::after Pseudo-Elements Explained

W
W3Tweaks Team
Frontend Tutorials
May 29, 2026 14 min read
CSS ::before and ::after Pseudo-Elements Explained
Decorative arrows, tooltips, notification badges, quote marks, and animated underlines — without adding a single extra span or div to your HTML. This guide explains how ::before and ::after work, 8 real use cases, and the 5 reasons they silently stop showing.

You can add decorative arrows, tooltips, notification badges, quote marks, and animated underlines to elements — without adding a single extra <span> or <div> to your HTML. That’s exactly what ::before and ::after are for.

Most developers learn the syntax quickly but stay confused about why their pseudo-elements randomly stop showing, or why width and height seem to be ignored. This guide covers both: what they are, how to use them with real examples, and a complete debug checklist for when they silently refuse to appear.

Live Demo

Live Demo Open in tab

Three tabs: toggle pseudo-element properties live and see the DOM tree, 8 real use cases, and the 5 reasons ::before/::after stops working with fixes.

What Are ::before and ::after?

::before and ::after are pseudo-elements — they insert virtual elements into the DOM that exist in the rendered page but are never written in your HTML. The browser creates them for you based on your CSS.

  • ::before inserts a virtual element as the first child of the selected element
  • ::after inserts a virtual element as the last child of the selected element
<!-- Your HTML -->
<div class="card">Hello</div>

<!-- What the browser renders -->
<div class="card">
  ::before  ← virtual, CSS-only
  Hello
  ::after   ← virtual, CSS-only
</div>

They behave like real elements — you can position them, give them dimensions, animate them, and apply any CSS property. The only difference is they’re created and controlled entirely in CSS.

Why Use ::before and ::after?

The core reason: keep your HTML clean. Instead of adding empty <span> elements for decorative arrows, label overlays, or decorative lines, pseudo-elements do the same job in CSS with zero extra markup.

/* Without pseudo-elements — cluttered HTML */
<button class="btn">
  Read more
  <span class="arrow"></span>  ← extra markup
</button>

/* With pseudo-elements — clean HTML */
<button class="btn">Read more</button>

.btn::after {
  content: " →";
}

This matters for accessibility too — screen readers don’t read ::before/::after content as meaningful text (depending on the content value), so they’re appropriate for purely decorative additions.

The content Property — The One Rule You Cannot Skip

Every ::before and ::after must have a content property. This is what tells the browser to render the pseudo-element at all. Without it, nothing shows — no error, no warning, just silence.

/* Invisible — content is missing */
.card::before {
  width: 8px;
  height: 8px;
  background: #a78bfa;
  border-radius: 50%;
}

/* Visible — content: "" is enough for a decorative shape */
.card::before {
  content: ""; /* required, even if empty */
  display: block;
  width: 8px;
  height: 8px;
  background: #a78bfa;
  border-radius: 50%;
}

The content property accepts several values:

/* Plain text */
.label::after  { content: " NEW"; }

/* Unicode characters */
.list li::before { content: "→ "; }

/* Empty string — for purely decorative shapes */
.shape::before { content: ""; }

/* HTML attribute value */
.tooltip::after { content: attr(data-tip); }

/* Counter value */
.step::before { content: counter(step-counter); }

/* URL (image) */
.icon::before { content: url("/icons/check.svg"); }

::before vs ::after — What’s the Difference?

The difference is purely about insertion position:

::before::after
Inserted asFirst child of elementLast child of element
Visual positionBefore the element’s contentAfter the element’s content
Common usesOpening quotes, lead icons, step numbersClosing quotes, arrows, tooltips, badges
z-index stackingSits below ::after when both are usedSits above ::before by default

In practice, the choice between the two is usually about which position makes more semantic and visual sense for your use case. For a tooltip that appears above the element, ::after is conventional. For a decorative opening quote, ::before makes more sense.

The Positioning Pattern

The most powerful use of pseudo-elements comes from combining them with position: absolute. This pattern unlocks overlays, badges, decorations, and effects without touching the HTML:

/* Step 1: position the parent */
.card {
  position: relative; /* required */
}

/* Step 2: position the pseudo-element inside it */
.card::after {
  content: "NEW";
  position: absolute;
  top: 12px;
  right: 12px;

  background: #a78bfa;
  color: #fff;
  font-size: 10px;
  font-weight: 700;
  padding: 2px 8px;
  border-radius: 4px;
  letter-spacing: 1px;
}

Without position: relative on the parent, the ::after element positions itself against the nearest positioned ancestor — which is almost never what you want. The CSS positioning guide covers exactly how absolute positioning resolves against ancestors.

8 Real Use Cases

1. Decorative Lines Around a Heading

Lines on either side of a section title — no extra <hr> elements needed:

.section-title {
  display: flex;
  align-items: center;
  gap: 16px;
  text-align: center;
}

.section-title::before,
.section-title::after {
  content: "";
  flex: 1;
  height: 1px;
  background: linear-gradient(90deg, transparent, rgba(167,139,250,0.5));
}

.section-title::after {
  background: linear-gradient(90deg, rgba(167,139,250,0.5), transparent);
}

2. Button with Animated Arrow

An arrow that slides on hover — no icon library required:

.btn {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 10px 20px;
}

.btn::after {
  content: "";
  display: inline-block;
  width: 8px;
  height: 8px;
  border-top: 2px solid currentColor;
  border-right: 2px solid currentColor;
  transform: rotate(45deg);
  transition: transform 0.25s ease;
}

.btn:hover::after {
  transform: rotate(45deg) translate(3px, -3px);
}

3. Tooltip Using attr()

A CSS-only tooltip that reads from an HTML data attribute:

<span class="tooltip" data-tip="Built with ::after and attr()">Hover me</span>
.tooltip {
  position: relative;
  cursor: default;
}

.tooltip::after {
  content: attr(data-tip); /* reads the data-tip attribute */
  position: absolute;
  bottom: calc(100% + 10px);
  left: 50%;
  transform: translateX(-50%);
  background: #1a2235;
  border: 1px solid rgba(167,139,250,0.3);
  color: #e2eaf5;
  font-size: 12px;
  padding: 6px 10px;
  border-radius: 6px;
  white-space: nowrap;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.2s;
}

.tooltip:hover::after {
  opacity: 1;
}

4. Notification Badge

A notification count badge using attr() — no JavaScript needed for the visual:

<button class="icon-btn" data-count="3">🔔</button>
.icon-btn {
  position: relative;
}

.icon-btn::after {
  content: attr(data-count);
  position: absolute;
  top: -6px;
  right: -6px;
  width: 20px;
  height: 20px;
  background: #f87171;
  border-radius: 50%;
  font-size: 10px;
  font-weight: 800;
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 2px solid var(--page-bg);
}

5. Content Label Overlay

A “NEW” or “SALE” label on a card without adding HTML:

.article-card {
  position: relative;
}

.article-card.is-new::after {
  content: "NEW";
  position: absolute;
  top: 12px;
  left: 12px;
  background: #a78bfa;
  color: #fff;
  font-size: 10px;
  font-weight: 800;
  padding: 3px 8px;
  border-radius: 4px;
  letter-spacing: 1.5px;
}

6. OR Divider Between Sections

The horizontal lines in login forms that say “or continue with”:

.divider {
  display: flex;
  align-items: center;
  gap: 12px;
  color: #5a6a82;
  font-size: 13px;
}

.divider::before,
.divider::after {
  content: "";
  flex: 1;
  height: 1px;
  background: rgba(255,255,255,0.07);
}

7. Animated Hover Underline

A smooth underline that grows from left to right on hover:

.nav-link {
  position: relative;
  text-decoration: none;
}

.nav-link::after {
  content: "";
  position: absolute;
  bottom: -2px;
  left: 0;
  width: 0;
  height: 2px;
  background: #a78bfa;
  transition: width 0.3s ease;
}

.nav-link:hover::after {
  width: 100%;
}

8. Decorative Opening Quote

A large quotation mark behind a blockquote — no image, no extra element:

blockquote {
  position: relative;
  padding: 16px 16px 16px 48px;
}

blockquote::before {
  content: "\201C"; /* Unicode left double quote */
  position: absolute;
  left: 8px;
  top: 0;
  font-size: 60px;
  line-height: 1;
  color: rgba(167,139,250,0.3);
  font-family: Georgia, serif;
}

Not Working? The 5 Reasons and Their Fixes

These are the only reasons ::before or ::after ever silently fails to show.

Reason 1 — Missing content property

/* Broken — pseudo-element doesn't render at all */
.icon::before {
  width: 12px;
  height: 12px;
  background: red;
}

/* Fixed */
.icon::before {
  content: ""; /* add this */
  display: block;
  width: 12px;
  height: 12px;
  background: red;
}

Reason 2 — Applied to a void/replaced element

::before and ::after insert content inside an element. Void elements (<img>, <input>, <br>, <hr>) cannot have children — pseudo-elements on them are ignored.

/* Broken — img cannot have children */
img::before { content: "NEW"; }

/* Fixed — wrap in a div */
.img-wrapper { position: relative; }
.img-wrapper::after {
  content: "NEW";
  position: absolute;
  top: 8px; left: 8px;
}

Reason 3 — Shape has no display set

Pseudo-elements default to display: inline. Inline elements ignore width, height, and vertical padding.

/* Broken — width/height ignored on inline */
.dot::before {
  content: "";
  width: 12px;
  height: 12px;
  background: #a78bfa;
}

/* Fixed */
.dot::before {
  content: "";
  display: block; /* or inline-block */
  width: 12px;
  height: 12px;
  background: #a78bfa;
}

Reason 4 — Parent missing position: relative

/* Broken — ::after escapes to nearest positioned ancestor */
.card { /* no position */ }
.card::after {
  content: "Label";
  position: absolute;
  top: 8px; right: 8px;
}

/* Fixed */
.card { position: relative; }
.card::after {
  content: "Label";
  position: absolute;
  top: 8px; right: 8px;
}

Reason 5 — Clipped by overflow: hidden

/* Broken — ::before extends outside and gets clipped */
.card {
  overflow: hidden;
  position: relative;
}
.card::before {
  content: "";
  position: absolute;
  inset: -4px; /* outside bounds — clipped */
}

/* Fixed — move pseudo-element to outer wrapper */
.card-wrapper { position: relative; }
.card-wrapper::before {
  content: "";
  position: absolute;
  inset: -4px;
}
.card { overflow: hidden; }

If overflow: hidden is breaking other things too — like sticky positioning — the position: sticky guide covers that whole trap and the overflow: clip workaround.

::before / ::after vs Pseudo-Classes

This is a common mix-up. Pseudo-elements (::before, ::after, ::placeholder, ::selection) target a part of an element or insert virtual elements. Pseudo-classes (:hover, :focus, :nth-child()) target elements based on their state or position in the DOM.

/* Pseudo-class — selects an element in a certain state */
.btn:hover { background: #6366f1; }

/* Pseudo-element — inserts a virtual element */
.btn::after { content: " →"; }

/* Combined — change the pseudo-element on state change */
.btn:hover::after { content: " ✓"; color: #34d399; }

The double colon (::) syntax for pseudo-elements is CSS3+. The single colon (:before, :after) was the CSS2 syntax — still supported in all browsers but not recommended. Use double colons.

Common Gotchas

1. You can only have one ::before and one ::after per element

/* The second ::before overwrites the first */
.card::before { content: "A"; }
.card::before { content: "B"; } /* replaces above */

/* Use both for two decorations */
.card::before { content: "A"; }
.card::after  { content: "B"; }

2. content: none and content: normal remove the pseudo-element

/* Useful for resetting in responsive layouts */
@media (max-width: 600px) {
  .section-title::before,
  .section-title::after {
    content: none; /* removes decorative lines on mobile */
  }
}

3. content values with attr() only work for string attributes

/* Works — data attributes are strings */
.tooltip::after { content: attr(data-tip); }

/* No unit support — can't do attr(data-width)px in stable CSS */

4. Pseudo-elements are ignored by querySelector in JavaScript

// This always returns null
document.querySelector('.card::before');

// Read computed styles instead
const styles = getComputedStyle(document.querySelector('.card'), '::before');
console.log(styles.getPropertyValue('content'));

5. Pseudo-elements create their own stacking interactions

When you start animating ::before overlays on hovered cards, layering surprises follow. The z-index and stacking contexts guide explains why pseudo-elements sometimes paint above content you expect them to sit behind.

Browser Support

::before and ::after are supported in every browser that handles CSS — Chrome, Firefox, Safari, Edge, and every mobile browser. There are no polyfills needed, no feature flags, and no caveats for modern browsers.

The double colon syntax (::before) is supported in all modern browsers. IE8 only supports the single-colon syntax (:before) — only relevant if you’re maintaining very legacy codebases. For exact compatibility data see MDN’s ::before reference and caniuse.com/css-gencontent.

Key Takeaways

  • ::before inserts a virtual first child, ::after inserts a virtual last child — both exist in the render, not in the HTML
  • The content property is required — even content: "" for purely decorative shapes
  • Pseudo-elements default to display: inline — set display: block or display: inline-block to use width and height
  • For positioned overlays, the parent must have position: relative
  • They cannot be used on void elements — <img>, <input>, <br>, <hr> — wrap these instead
  • overflow: hidden on the parent clips absolutely-positioned pseudo-elements — move to an outer wrapper
  • Use ::before/::after for decorative content; avoid putting meaningful content in them for accessibility
  • The double colon :: syntax is correct — single colon is legacy CSS2

FAQ

What is ::before and ::after in CSS?

They are pseudo-elements that insert virtual child elements at the beginning (::before) and end (::after) of a selected element’s content. They exist in the rendered page but are created entirely in CSS — no HTML required. You control their content with the content property and style them like any other element.

Why use ::before and ::after?

The main benefit is keeping HTML clean and semantic. Decorative elements like arrows, lines, badges, quote marks, and overlay labels are pure presentation — they belong in CSS, not HTML. Pseudo-elements let you add them without empty <span> elements cluttering your markup.

How do I use ::before and ::after in CSS?

Select an element, append ::before or ::after, and always include a content property. For decorative shapes, set content: "". For positioned overlays, add position: absolute and make sure the parent has position: relative:

.parent { position: relative; }
.parent::after {
  content: "";
  position: absolute;
  top: 0; right: 0;
  width: 20px; height: 20px;
  background: #a78bfa;
}

Why is my ::before or ::after not showing?

Five reasons in order of likelihood: (1) content property is missing — add content: "" at minimum, (2) the element is a void element like <img> or <input> — wrap it in a <div> instead, (3) display is inline by default — add display: block if using width/height, (4) the parent is missing position: relative — required for absolute positioning, (5) an ancestor has overflow: hidden that is clipping it — move to an outer wrapper.

What is the difference between ::before and ::after?

::before inserts its virtual element as the first child — before the element’s content. ::after inserts it as the last child — after the content. For position: absolute use cases, visual position is controlled by top, right, bottom, and left, so the before/after distinction is less important than it is for inline use.

Can I use ::before and ::after on an img tag?

No. <img> is a void element — it cannot have child elements, and pseudo-elements are children. The fix is to wrap the image in a <div> or <figure> and apply ::before/::after to the wrapper instead.

What is the difference between pseudo-elements and pseudo-classes?

Pseudo-classes (:hover, :focus, :nth-child) target elements based on their state or position in the DOM — nothing is created. Pseudo-elements (::before, ::after, ::placeholder) target a specific part of an element, or in the case of ::before/::after, create new virtual elements. You can combine them: .btn:hover::after changes the ::after pseudo-element when the button is hovered.