What CSS ::before and ::after are

::before and ::after are pseudo-elements. Think of them like “extra helper elements” that CSS can create for you inside an element.

  • ::before becomes the first child of the element.
  • ::after becomes the last child of the element.

They’re perfect for decoration: icons, badges, highlights, custom borders, little arrows, and other “UI sprinkles” you don’t want to add extra HTML for.

Important: ::before and ::after are not real HTML nodes you can select in JavaScript as elements. They’re rendered by CSS.

 .note::before { content: "Before → "; } .note::after { content: " ← After"; } 
 .note::before { content: "✅ "; } .note::after { content: " (nice)"; } 
 .note::before { content: "✨ "; } .note::after { content: ""; } 
 *, ::before, ::after { box-sizing: border-box; } .note { font-family: ui-monospace, SFMono-Regular, monospace; padding: 14px 16px; border: 2px solid #111; background: #f2f2f2; width: min(560px, 100%); } 
 

This paragraph has pseudo-elements.

The one rule you can’t skip: content

A pseudo-element usually won’t show up unless you set content. This is the #1 beginner gotcha.

  • content: "Hello"; shows text.
  • content: ""; creates an “empty” pseudo-element that can still be styled (great for shapes).
  • content: none; removes it.
 .badge::after { content: "NEW"; } 
 .badge::after { content: ""; } 
 .badge::after { content: none; } 
 *, ::before, ::after { box-sizing: border-box; } .badge { font-family: ui-monospace, SFMono-Regular, monospace; border: 2px solid #111; padding: 12px 16px; width: min(560px, 100%); background: #f2f2f2; } .badge::after { margin-left: 10px; font-size: 12px; padding: 2px 8px; border: 2px solid #111; background: white; } 
 
Product name

Styling basics: making your pseudo-element visible

Once you have content, you can style pseudo-elements like tiny boxes. The most common steps are:

  1. Set content.
  2. Give it size (often display plus width/height).
  3. Place it (normal flow or position).

By default, pseudo-elements behave like inline content. If you want a box shape, you typically add display: inline-block; (or block).

 .dot::before { content: ""; display: inline-block; width: 12px; height: 12px; border: 2px solid #111; border-radius: 999px; margin-right: 10px; } 
 .dot::before { content: ""; display: inline-block; width: 16px; height: 16px; background: #111; border-radius: 4px; margin-right: 10px; } 
 .dot::before { content: "→"; display: inline-block; margin-right: 10px; } 
 *, ::before, ::after { box-sizing: border-box; } .dot { font-family: ui-monospace, SFMono-Regular, monospace; border: 2px solid #111; background: #f2f2f2; padding: 14px 16px; width: min(560px, 100%); } 
 
This is a label with a pseudo-element.

Positioning 101: absolute positioning with pseudo-elements

The classic pattern is: make the parent position: relative;, then absolutely position the pseudo-element inside it.

  • Parent: position: relative; (creates the positioning reference)
  • Pseudo-element: position: absolute; + offsets like top, right, bottom, left
 .card::after { content: ""; position: absolute; top: 10px; right: 10px; width: 14px; height: 14px; border: 2px solid #111; border-radius: 999px; } 
 .card::after { content: ""; position: absolute; bottom: 10px; left: 10px; width: 60px; height: 6px; background: #111; } 
 .card::after { content: "★"; position: absolute; top: 8px; right: 10px; font-size: 20px; } 
 *, ::before, ::after { box-sizing: border-box; } .card { position: relative; font-family: ui-monospace, SFMono-Regular, monospace; border: 2px solid #111; background: #f2f2f2; padding: 16px; width: min(560px, 100%); min-height: 140px; } .card p { margin: 0; max-width: 44ch; } 
 

This card has an ::after pseudo-element positioned inside it.

Centering tricks: transform translate

A very common trick is centering a pseudo-element by placing its top-left corner at 50% / 50%, then pulling it back by half of its own size using transform.

This works well for overlays, play buttons, decorative blobs, and “spotlight” effects.

 .panel::before { content: ""; position: absolute; top: 50%; left: 50%; width: 140px; height: 140px; transform: translate(-50%, -50%); border: 3px solid #111; border-radius: 999px; } 
 .panel::before { content: "PLAY"; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); padding: 10px 16px; border: 3px solid #111; background: white; font-size: 14px; } 
 .panel::before { content: ""; position: absolute; top: 50%; left: 50%; width: 90%; height: 60%; transform: translate(-50%, -50%); background: #111; opacity: 0.08; } 
 *, ::before, ::after { box-sizing: border-box; } .panel { position: relative; font-family: ui-monospace, SFMono-Regular, monospace; border: 2px solid #111; background: #f2f2f2; width: min(560px, 100%); height: 220px; padding: 16px; } 
 
Centering an element that doesn’t exist in your HTML? Pseudo-elements to the rescue.

Badges with data-attributes: dynamic text without extra markup

You can pull text from an HTML attribute using attr(). This is super useful for badges, labels, and small UI text.

  • HTML: data-status="Hot"
  • CSS: content: attr(data-status);

Note: attr() is widely used for content. Using attr() for other CSS properties is a more advanced feature and not consistently supported everywhere, so keep it to content.

 .product::after { content: attr(data-status); position: absolute; top: 10px; right: 10px; font-size: 12px; padding: 4px 10px; border: 2px solid #111; background: white; } 
 .product::after { content: attr(data-status); position: absolute; top: 10px; right: 12px; font-size: 12px; padding: 4px 10px; border: 2px solid #111; background: #111; color: white; } 
 .product::after { content: "Status: " attr(data-status); position: absolute; bottom: 10px; right: 10px; font-size: 12px; padding: 4px 10px; border: 2px solid #111; background: white; } 
 *, ::before, ::after { box-sizing: border-box; } .product { position: relative; font-family: ui-monospace, SFMono-Regular, monospace; border: 2px solid #111; background: #f2f2f2; width: min(560px, 100%); padding: 18px 16px; min-height: 140px; } .product h4 { margin: 0 0 10px 0; font-size: 16px; } .product p { margin: 0; max-width: 46ch; } 
 

Wireless Keyboard

This card reads a status from data-status and displays it using ::after.

Tooltips: practical ::after in the real world

Tooltips are a perfect pseudo-element exercise: the tooltip text is “extra UI”, so it’s nice not to clutter the HTML.

Pattern:

  1. Store tooltip text in data-tip.
  2. Use ::after to render it.
  3. Hide it by default, show it on :hover (and :focus for keyboard users).
 .tip::after { content: attr(data-tip); position: absolute; left: 50%; bottom: calc(100% + 10px); transform: translateX(-50%); padding: 8px 10px; border: 2px solid #111; background: white; font-size: 12px; white-space: nowrap; opacity: 0; pointer-events: none; z-index: 10;}

.tip:hover::after,
.tip:focus::after {
opacity: 1;
}
 .tip::after { content: attr(data-tip); position: absolute; left: 0; top: calc(100% + 10px); padding: 8px 10px; border: 2px solid #111; background: white; font-size: 12px; max-width: 220px; opacity: 0; pointer-events: none; z-index: 10;} .tip:hover::after, .tip:focus::after { opacity: 1; } 
 .tip::after { content: attr(data-tip); position: absolute; right: 0; bottom: calc(100% + 10px); padding: 8px 10px; border: 2px solid #111; background: #111; color: white; font-size: 12px; opacity: 0; pointer-events: none; z-index: 10;} .tip:hover::after, .tip:focus::after { opacity: 1; } 
 *, ::before, ::after { box-sizing: border-box; } .wrap { font-family: ui-monospace, SFMono-Regular, monospace; width: min(560px, 100%); padding: 18px; border: 2px solid #111; background: #f2f2f2; margin-top: 70px;} .tip { position: relative; display: inline-block; padding: 6px 10px; border: 2px solid #111; background: white; cursor: help; outline: none; } .wrap p { margin: 0 0 14px 0; } .hint { opacity: 0.75; font-size: 12px; margin: 0; } 
 

Hover or focus: this thing

Tip: pointer-events: none stops the tooltip from “stealing” your hover.

Adding a tooltip arrow with ::before + ::after

Tooltips often have a little arrow. We can create it with ::before while the tooltip box stays in ::after.

The arrow is just a small triangle made with border CSS.

/* tooltip box */
.tip::before {
  content: attr(data-tip);

  position: absolute;
  left: 50%;
  bottom: calc(100% + 22px); /* gap (12px) + arrow height (10px) */
  transform: translateX(-50%) translateY(6px);

  max-width: 260px;
  width: max-content;

  background: #111;
  color: #fff;
  border: 2px solid #111;
  border-radius: 10px;
  padding: 10px 12px;

  font-size: 14px;
  line-height: 1.3;
  white-space: normal;
  text-align: center;

  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity 0.18s ease, transform 0.18s ease, visibility 0s linear 0.18s;
  z-index: 10;
}

/* arrow (border-colored triangle) */
.tip::after {
  content: "";

  position: absolute;
  left: 50%;
  bottom: calc(100% + 12px); /* gap */
  transform: translateX(-50%) translateY(6px);

  width: 0;
  height: 0;
  border-left: 12px solid transparent;
  border-right: 12px solid transparent;
  border-top: 10px solid #111; /* matches the tooltip border */

  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity 0.18s ease, transform 0.18s ease, visibility 0s linear 0.18s;
  z-index: 9;
}

/* show on hover + keyboard focus */
.tip:hover::before,
.tip:hover::after,
.tip:focus-visible::before,
.tip:focus-visible::after {
  opacity: 1;
  visibility: visible;
  transform: translateX(-50%) translateY(0);
  transition-delay: 0s;
}
 *, ::before, ::after { box-sizing: border-box; } .wrap { font-family: ui-monospace, SFMono-Regular, monospace; width: min(560px, 50%);
  padding: 18px;
  border: 2px solid #111;
  background: #f2f2f2;
  margin: 60px 0 0 100px;} .tip { position: relative; display: inline-block; padding: 6px 10px; border: 2px solid #111; background: white; cursor: help; outline: none; } 
 
Hover me

Here we are using CSS transitions to smoothly animate the tooltip's appearance and disappearance. Learn more in the CSS Transition Interactive Tutorial

Fancy borders and highlights without extra markup

Sometimes you want an “extra border” or a highlight behind text. Pseudo-elements are great because you don’t need wrapper divs.

Two common patterns:

  • A background highlight behind content: put ::before behind using z-index.
  • A double border: create an inset rectangle using ::after.
 .title { position: relative; isolation: isolate;}

.title::before {
content: "";
position: absolute;
left: -6px;
right: -6px;
bottom: 2px;
height: 14px;
background: #111;
opacity: 0.18;
z-index: -1;
}
 .box { position: relative; } .box::after { content: ""; position: absolute; inset: 10px; border: 2px solid #111; opacity: 0.35; } 
 .box { position: relative; } .box::before { content: ""; position: absolute; inset: -6px; border: 2px dashed #111; opacity: 0.4; } 
 *, ::before, ::after { box-sizing: border-box; } .wrap { font-family: ui-monospace, SFMono-Regular, monospace; width: min(560px, 100%); padding: 18px; border: 2px solid #111; background: #f2f2f2; } .title { display: inline-block; margin: 0 0 16px 0; font-size: 18px; } .box { border: 2px solid #111; background: white; padding: 16px; min-height: 120px; } .box p { margin: 0; max-width: 48ch; } 
 

Highlighted title

This box uses pseudo-elements for “extra border” effects without adding extra wrapper HTML.

In the first snippet, we are using isolation: isolate; on the parent to create a new stacking context, which helps with layering pseudo-elements behind content. Thanks to isolation: isolate; the pseudo element is behind the text, but still above the background. Learn more in the CSS Stacking Context Interactive Tutorial

Full overlay: darken an image or add a tint

A pseudo-element can cover an entire element. This is useful for overlays, tints, and “glass” effects.

The important property here is inset: 0;, which is a shortcut for top: 0; right: 0; bottom: 0; left: 0;.

 .hero::before { content: ""; position: absolute; inset: 0; background: #111; opacity: 0.25; } 
 .hero::before { content: ""; position: absolute; inset: 0; background: #111; opacity: 0.12; } .hero::after { content: ""; position: absolute; inset: 12px; border: 2px solid #111; opacity: 0.35; } 
 .hero::before { content: ""; position: absolute; inset: 0; background: #111; opacity: 0.2; } .hero .text { position: relative; } 
 *, ::before, ::after { box-sizing: border-box; } .hero { position: relative; width: min(560px, 100%); height: 220px; border: 2px solid #111; overflow: hidden; font-family: ui-monospace, SFMono-Regular, monospace; background-image: url("https://picsum.photos/900/500"); background-size: cover; background-position: center; padding: 16px; } .text { color: #111; background: rgba(255, 255, 255, 0.85); border: 2px solid #111; padding: 10px 12px; width: fit-content; } 
 
Overlay using ::before

Layering: z-index, stacking, and “why is it behind?”

Pseudo-elements are affected by layering rules. If you’re doing overlays and highlights, you’ll run into z-index.

  • z-index only works on positioned elements (like position: relative; or absolute).
  • A common highlight trick is putting ::before behind text using z-index: -1;.
  • If your pseudo-element disappears, it may be behind a parent background or clipped by overflow: hidden;.

If you are having issues with z-index, learn more in the CSS Z-Index Interactive Tutorial.

In this playground, try the snippets and notice which layers sit above or below the text.

 .card { position: relative; }

.card::before {
content: "";
position: absolute;
inset: 0;
background: #111;
opacity: 0.12;
}

.card .label {
position: relative;
}
 .card { position: relative; } .card::before { content: ""; position: absolute; inset: 0; background: #111; opacity: 0.12; z-index: -1; } .card .label { position: relative; } 
 .card { position: relative; } .card::before { content: ""; position: absolute; inset: 0; background: #111; opacity: 0.12; z-index: 2; } .card .label { position: relative; z-index: 1; } 
 *, ::before, ::after { box-sizing: border-box; } .card { width: min(560px, 100%); border: 2px solid #111; background: #f2f2f2; padding: 16px; font-family: ui-monospace, SFMono-Regular, monospace; min-height: 140px; } .label { display: inline-block; padding: 6px 10px; border: 2px solid #111; background: white; } 
 
Layer test

Click-through overlays: pointer-events

If you create a full overlay pseudo-element, it can block clicks on buttons or links underneath it. When you want the overlay to be “visual only”, add:

pointer-events: none;

 .banner::before { content: ""; position: absolute; inset: 0; background: #111; opacity: 0.12; } 
 .banner::before { content: ""; position: absolute; inset: 0; background: #111; opacity: 0.12; pointer-events: none; } 
 *, ::before, ::after { box-sizing: border-box; } .banner { position: relative; width: min(560px, 100%); border: 2px solid #111; background: #f2f2f2; padding: 16px; font-family: ui-monospace, SFMono-Regular, monospace; } .button { display: inline-block; padding: 10px 12px; border: 2px solid #111; background: white; text-decoration: none; color: #111; } 
  

Notice how you can click the button with the second code snippet active, thanks to pointer-events: none;.

Common mistakes and a debugging checklist

If your ::before or ::after isn’t showing, run through this list:

  1. Did you set content? (Even content: ""; counts.)
  2. Are you expecting it to have size? If yes, add display and width/height.
  3. Are you positioning it absolutely? If yes, does the parent have position: relative;?
  4. Is it hidden behind something due to z-index or clipped by overflow: hidden;?
  5. Did you accidentally use single-colon syntax like :before? (It often still works, but modern CSS uses ::before.)

And a quick pro tip: temporarily add background: #111; and opacity: 0.2; to your pseudo-element. It’s like turning on a flashlight while debugging.

 .mystery::before { content: ""; position: absolute; inset: 10px; background: #111; opacity: 0.2; } 
 .mystery::before { content: "I exist"; position: absolute; top: 10px; left: 10px; padding: 6px 10px; border: 2px solid #111; background: white; } 
 .mystery::before { content: ""; display: inline-block; width: 18px; height: 18px; border: 2px solid #111; margin-right: 10px; } 
 *, ::before, ::after { box-sizing: border-box; } .mystery { position: relative; width: min(560px, 100%); border: 2px solid #111; background: #f2f2f2; padding: 16px; font-family: ui-monospace, SFMono-Regular, monospace; min-height: 140px; display: flex; align-items: center; } 
Debug me: where will  ::before appear?

A Few More Examples

Each example is a small real-world scenario that uses pseudo-elements.

Example 1: notification dot

Here, we add a little dot on the top-right corner of the avatar, like a notification indicator.

 .avatar { position: relative; }

.avatar::after {
content: "";
position: absolute;
top: -4px;
right: -4px;
width: 14px;
height: 14px;
border: 2px solid #111;
background: #111;
border-radius: 999px;
}
 .avatar { position: relative; } .avatar::after { content: ""; position: absolute; top: -4px; right: -4px; width: 14px; height: 14px; border: 2px solid #111; background: white; border-radius: 999px; } 
 *, ::before, ::after { box-sizing: border-box; } .avatar { width: 120px; height: 120px; border: 2px solid #111; border-radius: 999px; background-image: url("https://picsum.photos/240/240"); background-size: cover; background-position: center; margin-top: 10px;} 
 

Example 2: quote decoration

Here, we add decorative quote marks around a blockquote using ::before and ::after.

 .quote::before { content: "“"; font-size: 40px; line-height: 1; margin-right: 8px; }

.quote::after {
content: "”";
font-size: 40px;
line-height: 1;
margin-left: 8px;
}
 .quote::before { content: "“"; position: absolute; top: 0; left: 12px; font-size: 48px; line-height: 1; } .quote::after { content: "”"; position: absolute; bottom: -26px; right: 12px; font-size: 48px; line-height: 1; } 
 *, ::before, ::after { box-sizing: border-box; } .quote { position: relative; width: min(560px, 100%); border: 2px solid #111; background: #f2f2f2; padding: 22px 18px; font-family: ui-monospace, SFMono-Regular, monospace; margin: 0; margin-top: 100px;} .quote p { margin: 0; max-width: 54ch; } 
 

Pseudo-elements are like cheat codes for clean HTML.

CSS ::before and ::after Pseudo-Elements Wrap-up

You now know the big ideas behind ::before and ::after:

  • They create extra renderable “boxes” without adding extra HTML.
  • They need content to exist.
  • With display, sizing, and position, they can become badges, overlays, borders, arrows, and more.

Next time you feel tempted to add a random <span> just for styling, pause for one second and ask: “Could this be a ::before or ::after moment?”