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.
-
::beforebecomes the first child of the element. -
::afterbecomes 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:
- Set
content. - Give it size (often
displaypluswidth/height). - 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 liketop,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
::afterpseudo-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-statusand 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:
- Store tooltip text in
data-tip. - Use
::afterto render it. - Hide it by default, show it on
:hover(and:focusfor 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: nonestops 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
::beforebehind usingz-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-indexonly works on positioned elements (likeposition: relative;orabsolute). - A common highlight trick is putting
::beforebehind text usingz-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:
- Did you set
content? (Evencontent: "";counts.) - Are you expecting it to have size? If yes, add
displayandwidth/height. - Are you positioning it absolutely? If yes, does the parent have
position: relative;? - Is it hidden behind something due to
z-indexor clipped byoverflow: hidden;? - 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::beforeappear?
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
contentto exist. - With
display, sizing, andposition, 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?”
