w3tweaks.com · CSS Tutorial

CSS ::before & ::after

How they work, real use cases, and why yours might not be showing.

Tab 1

What ::before and ::after Actually Are

They're virtual elements the browser inserts inside your element — one before the content, one after. They exist in the rendered page but not in your HTML. Toggle the properties below to see them live.

DOM Tree — What the Browser Sees
<div class="card"> real element
::before virtual — CSS only
"Hello "  ← content property
Actual HTML content here
::after virtual — CSS only
""  ← required, even if empty
</div>
Live Preview — Toggle Properties
w3tweaks
The content Property — What You Can Put In It
Read the docs
Task complete
Hover me
Empty but visible
Quoted text
The one rule you cannot skip: content is required on every ::before and ::after. Even if you want a purely decorative shape with no text, you must write content: "". Without it, the pseudo-element simply does not render — no error, just nothing.
Tab 2

8 Real Use Cases

These are the patterns developers actually use every day — all built with ::before and ::after, zero extra HTML elements.

✦ Decorative Lines Around Text
CSS Tutorials

The lines are ::before and ::after with flex: 1 and height: 1px.

→ Button with Animated Arrow
Read Tutorial

The arrow is ::after with border-top + border-right rotated 45°.

💬 Tooltip with attr()
Hover over me

content: attr(data-tip) reads the HTML attribute

🔴 Notification Badge
🔔

Badge uses ::after with position: absolute + content: attr(data-count)

🏷️ Content Label Overlay
Article thumbnail here

"NEW" label is ::after on the wrapper — no extra HTML span needed.

── OR Divider ──
or continue with

Both lines are ::before + ::after with flex: 1.

~~ Hover Underline Effect ~~
Hover over this link

Underline is ::after with width: 0 growing to 100% on hover.

" Decorative Quotation Mark
You don't need extra HTML for decorative elements. That's what pseudo-elements are for.

Opening quote is ::before with content: "\201C".

Tab 3

Not Working? Here's Why

Every case of ::before or ::after silently not showing comes down to one of these five reasons. Click each one to see the broken code and the fix.

Reason 1 — Missing content property

This is the #1 reason. The content property is not optional — it is what tells the browser to render the pseudo-element at all. Even a purely decorative shape needs content: "".

❌ Broken
.card::before {
  /* no content */
  width: 8px;
  height: 8px;
  background: red;
}
✅ Fixed
.card::before {
  content: "";
  width: 8px;
  height: 8px;
  background: red;
}
Reason 2 — Used on a void/replaced element (img, input)

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

❌ Broken
/* img can't have children */
img::before {
  content: "NEW";
}

/* input can't either */
input::after {
  content: "";
}
✅ Fixed
/* Wrap img in a div */
.img-wrap {
  position: relative;
}
.img-wrap::after {
  content: "NEW";
  position: absolute;
}
Reason 3 — Decorative shape has no display set

Pseudo-elements default to display: inline. Inline elements ignore width, height, and vertical padding. If you're making a decorative block shape, you must set display: block or display: inline-block.

❌ Broken (invisible shape)
.dot::before {
  content: "";
  /* display: inline — default */
  width: 12px;
  height: 12px;
  /* width/height ignored */
}
✅ Fixed
.dot::before {
  content: "";
  display: block;
  width: 12px;
  height: 12px;
  background: #a78bfa;
}
Reason 4 — Forgot position: relative on the parent

When you use position: absolute on a pseudo-element to overlay it on the parent, the parent must have position: relative. Without it, the pseudo-element positions against the nearest positioned ancestor — often escaping the parent entirely.

❌ Broken
.card {
  /* no position set */
}

.card::after {
  content: "NEW";
  position: absolute;
  top: 8px;
  /* escapes .card! */
}
✅ Fixed
.card {
  position: relative;
}

.card::after {
  content: "NEW";
  position: absolute;
  top: 8px;
  /* stays inside .card ✓ */
}
Reason 5 — Hidden by overflow: hidden on parent

If position: absolute pseudo-elements extend outside the parent's bounds and the parent has overflow: hidden, the pseudo-element gets clipped. This is especially common with decorative glows, halos, and border effects.

❌ Broken (clipped)
.card {
  overflow: hidden;
  position: relative;
}

.card::before {
  content: "";
  position: absolute;
  inset: -8px;
  /* clipped by overflow */
}
✅ Fixed
.card-wrap {
  position: relative;
  /* no overflow here */
}
.card {
  overflow: hidden;
}
.card-wrap::before {
  /* now on wrapper ✓ */
}
Quick checklist: (1) content property present? (2) Not on img/input? (3) display: block set if using width/height? (4) Parent has position: relative? (5) No overflow: hidden clipping it?
Read the tutorial