What are CSS pseudo-elements?
A pseudo-element is like a “virtual” element that CSS can create for you, without adding anything to your HTML. You style it as if it exists, and the browser renders it as part of the element.
The biggest win: you can add decorations, labels, overlays, and tiny UI details without littering your markup with extra <span> tags.
Pseudo-elements are different from pseudo-classes:
-
Pseudo-classes describe a state (like
:hover,:focus,:nth-child()). -
Pseudo-elements describe a part of an element (like
::before,::after,::first-letter).
.note::before {
content: "Pseudo-element:";
font-weight: 700;
}
.note::after {
content: " (I’m not in your HTML)";
font-style: italic;
opacity: 0.8;
}
.note::before,
.note::after {
content: "";
display: inline-block;
width: 0.8em;
height: 0.8em;
border-radius: 999px;
background: #ef233c;
margin-inline: 0.35em;
transform: translateY(1px);
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
max-width: 980px;
padding: 18px;
font-family: system-ui, Arial, sans-serif;
display: grid;
gap: 12px;
}
.note {
border: 3px solid #111;
border-radius: 16px;
padding: 14px 16px;
background: #fff;
box-shadow: 0 12px 0 #111;
line-height: 1.35;
}
This sentence is plain HTML.
Meet the stars: CSS pseudo-elements ::before and ::after
::before and ::after create a box inside the element, at the beginning or end of its content.
Think of them as “extra layers” you can style.
- They do not exist in the DOM, but they render like real boxes.
-
They usually need
contentto show up (we’ll cover that next). - They’re perfect for decorative UI: tags, ribbons, shadows, highlights, icons, overlays.
::before and ::after are inline by default
If you want them to behave like a box, set display (commonly block or inline-block).
.card::before {
content: "New";
}
.card::before {
content: "New";
display: inline-block;
padding: 6px 10px;
border: 2px solid #111;
border-radius: 999px;
font-weight: 700;
margin-bottom: 10px;
}
.card::before {
content: "New";
display: block;
padding: 10px 12px;
border-bottom: 3px solid #111;
font-weight: 700;
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
max-width: 980px;
padding: 18px;
font-family: system-ui, Arial, sans-serif;
}
.card {
border: 3px solid #111;
border-radius: 18px;
background: #fff;
box-shadow: 0 12px 0 #111;
padding: 14px 16px;
width: min(560px, 100%);
}
.card h4 {
margin: 0 0 6px;
font-size: 18px;
}
.card p {
margin: 0;
opacity: 0.85;
line-height: 1.4;
}
Card title
This card is normal HTML. The “New” label is a pseudo-element.
The content property: the “on” switch
For ::before and ::after, content is usually required.
If you forget it, nothing appears, and you’ll spend a full minute blaming the universe.
Common content values
-
Text:
content: "Hello"; -
Empty string for purely decorative shapes:
content: ""; -
Attributes:
content: attr(data-label); -
Counters:
content: counter(step);
.tag::before {
content: "Text label";
}
.tag::before {
content: "";
display: inline-block;
width: 10px;
height: 10px;
border-radius: 999px;
background: #0077b6;
margin-right: 8px;
}
.tag::before {
content: attr(data-label);
font-weight: 700;
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
max-width: 980px;
padding: 18px;
font-family: system-ui, Arial, sans-serif;
display: grid;
gap: 12px;
}
.tag {
border: 3px solid #111;
border-radius: 999px;
padding: 10px 14px;
background: #fff;
width: fit-content;
box-shadow: 0 10px 0 #111;
}
.mini {
opacity: 0.85;
margin: 0;
}
This text is inside the element.
Positioning pseudo-elements like a pro
Most of the fun pseudo-element tricks use positioning:
make the parent position: relative, then place the pseudo-element with position: absolute.
Overlay badge in the corner
.tile::before {
content: "PRO";
position: absolute;
top: 12px;
right: 12px;
padding: 6px 10px;
border: 2px solid #111;
border-radius: 999px;
background: #fff;
font-weight: 800;
}
.tile::before {
content: "PRO";
position: absolute;
inset: 12px 12px auto auto;
padding: 6px 10px;
border: 2px solid #111;
border-radius: 999px;
background: #ef233c;
color: #111;
font-weight: 900;
box-shadow: 0 8px 0 #111;
}
.tile::before {
content: "PRO";
position: absolute;
top: 12px;
right: 12px;
padding: 6px 10px;
border: 2px solid #111;
border-radius: 999px;
background: #fff;
font-weight: 800;
transform: rotate(8deg);
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
max-width: 980px;
padding: 18px;
font-family: system-ui, Arial, sans-serif;
}
.tile {
position: relative;
width: min(560px, 100%);
border: 3px solid #111;
border-radius: 18px;
overflow: hidden;
background: #fff;
box-shadow: 0 12px 0 #111;
}
.tile img {
display: block;
width: 100%;
height: 240px;
object-fit: cover;
}
.tile .body {
padding: 14px 16px;
}
.tile h4 {
margin: 0 0 6px;
font-size: 18px;
}
.tile p {
margin: 0;
opacity: 0.85;
line-height: 1.4;
}
![]()
Premium article
The badge is
::before, positioned absolutely.
Stacking order: z-index and clicks
A pseudo-element can sit over content (overlay) or behind it (highlight). Two tips:
-
Use
z-indexto control stacking. -
If an overlay blocks clicking, add
pointer-events: none.
.cta::before {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(90deg, rgba(239, 35, 60, 0.35), rgba(0, 119, 182, 0.35));
pointer-events: none;
}
.cta::before {
content: "";
position: absolute;
inset: -10px;
background: rgba(0, 0, 0, 0.08);
z-index: -1;
border-radius: 22px;
}
.cta::after {
content: "";
position: absolute;
inset: auto 18px 14px 18px;
height: 3px;
background: #111;
opacity: 0.9;
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
max-width: 980px;
padding: 18px;
font-family: system-ui, Arial, sans-serif;
}
.cta {
position: relative;
width: min(560px, 100%);
border: 3px solid #111;
border-radius: 18px;
background: #fff;
box-shadow: 0 12px 0 #111;
padding: 18px;
display: grid;
gap: 10px;
isolation: isolate;
}
.cta h4 {
margin: 0;
font-size: 18px;
}
.cta p {
margin: 0;
opacity: 0.85;
line-height: 1.4;
}
.cta a {
width: fit-content;
display: inline-block;
border: 2px solid #111;
border-radius: 999px;
padding: 10px 14px;
font-weight: 800;
text-decoration: none;
color: #111;
background: #fff;
}
Overlay, background, underline
Try each snippet. Notice how pseudo-elements can sit above, behind, or act like extra decoration.
Clickable button
Learn more in the CSS Z-Index Interactive Tutorial.
Common patterns you’ll reuse everywhere
Pattern 1: Gradient image overlay
The classic “text over image” hero usually uses an overlay to keep text readable. A pseudo-element is perfect because it’s purely decorative.
.hero::before {
content: "";
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.35);
pointer-events: none;
}
.hero::before {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(180deg, rgba(0, 0, 0, 0.15), rgba(0, 0, 0, 0.65));
pointer-events: none;
}
.hero::before {
content: "";
position: absolute;
inset: 0;
background: radial-gradient(circle at 20% 20%, rgba(239, 35, 60, 0.45), rgba(0, 0, 0, 0.65));
mix-blend-mode: multiply;
pointer-events: none;
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
max-width: 980px;
padding: 18px;
font-family: system-ui, Arial, sans-serif;
}
.hero {
position: relative;
width: min(720px, 100%);
border: 3px solid #111;
border-radius: 18px;
overflow: hidden;
box-shadow: 0 12px 0 #111;
background: url("https://picsum.photos/1200/700") center / cover no-repeat;
min-height: 280px;
padding: 18px;
display: grid;
align-content: end;
gap: 6px;
color: #fff;
}
.hero h4 {
margin: 0;
font-size: 20px;
text-shadow: 0 2px 0 rgba(0, 0, 0, 0.35);
z-index: 1;
}
.hero p {
margin: 0;
opacity: 0.92;
line-height: 1.35;
max-width: 52ch;
text-shadow: 0 2px 0 rgba(0, 0, 0, 0.35);
}
Readable text on images
The overlay is decorative, so it belongs in a pseudo-element.
Pattern 2: A highlight behind text
Want that “marker highlight” behind a word? Use ::before behind the text with a lower z-index.
.mark {
position: relative;
isolation: isolate;
}
.mark::before {
content: "";
position: absolute;
left: -4px;
right: -4px;
bottom: 0.12em;
height: 0.55em;
background: rgba(239, 35, 60, 0.35);
z-index: -1;
transform: rotate(-2deg);
}
.mark {
position: relative;
isolation: isolate;
}
.mark::before {
content: "";
position: absolute;
inset: auto -6px 0.12em -6px;
height: 0.6em;
background: rgba(0, 119, 182, 0.32);
z-index: -1;
border-radius: 10px;
}
.mark {
position: relative;
}
.mark::after {
content: "";
position: absolute;
inset: auto 0 0 0;
height: 3px;
background: #111;
opacity: 0.7;
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
max-width: 980px;
padding: 18px;
font-family: system-ui, Arial, sans-serif;
}
.box {
width: min(720px, 100%);
border: 3px solid #111;
border-radius: 18px;
background: #fff;
box-shadow: 0 12px 0 #111;
padding: 18px;
line-height: 1.5;
}
.box p {
margin: 0;
font-size: 18px;
}
Pseudo-elements are great for pure decoration without extra HTML.
Pattern 3: Tooltip with attr()
Tooltips are a fun demo for pseudo-elements. In real projects, tooltips have accessibility concerns, but as an interactive CSS lesson this is perfect.
-
The tooltip text comes from
data-tipusingattr(data-tip). -
We show it on
:hoverand:focus-visible.
.tip {
position: relative;
}
.tip::after {
content: attr(data-tip);
position: absolute;
left: 50%;
bottom: calc(100% + 20px);
transform: translateX(-50%);
padding: 10px 12px;
border: 2px solid #111;
border-radius: 12px;
background: #fff;
box-shadow: 0 10px 0 #111;
width: max-content;
max-width: 240px;
opacity: 0;
pointer-events: none;
}
.tip::before {
content: "";
position: absolute;
left: 50%;
bottom: calc(100% + 6px);
width: 10px;
height: 10px;
background: #111;
border-left: 2px solid #111;
border-top: 2px solid #111;
transform: translateX(-50%) rotate(225deg);
opacity: 0;
pointer-events: none;
}
.tip:hover::after, .tip:hover::before, .tip:focus-visible::after, .tip:focus-visible::before {
opacity: 1;
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
max-width: 980px;
padding: 78px;
font-family: system-ui, Arial, sans-serif;
}
.row {
display: grid;
gap: 10px;
justify-items: start;
}
.tip {
border: 3px solid #111;
border-radius: 999px;
background: #fff;
box-shadow: 0 12px 0 #111;
padding: 12px 16px;
font-weight: 800;
color: #111;
}
.tip:focus-visible {
outline: 4px solid #0077b6;
outline-offset: 4px;
}
Pattern 4: Numbered steps with counters
CSS counters let you number items without manually typing 1, 2, 3…
The numbers can be rendered via ::before.
.steps {
counter-reset: step;
}
.steps li {
counter-increment: step;
}
.steps li::before {
content: counter(step);
display: inline-flex;
justify-content: center;
align-items: center;
width: 28px;
height: 28px;
border: 2px solid #111;
border-radius: 999px;
font-weight: 900;
margin-right: 10px;
}
.steps {
counter-reset: step;
}
.steps li {
counter-increment: step;
}
.steps li::before {
content: "Step " counter(step) ":";
font-weight: 900;
margin-right: 10px;
}
.steps {
counter-reset: step;
}
.steps li {
counter-increment: step;
}
.steps li::before {
content: counter(step) ".";
font-weight: 900;
margin-right: 10px;
color: #ef233c;
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
max-width: 980px;
padding: 18px;
font-family: system-ui, Arial, sans-serif;
}
.panel {
width: min(720px, 100%);
border: 3px solid #111;
border-radius: 18px;
background: #fff;
box-shadow: 0 12px 0 #111;
padding: 16px;
}
.steps {
margin: 0;
padding-left: 18px;
display: grid;
gap: 10px;
}
.steps li {
list-style: none;
border: 2px dashed rgba(0, 0, 0, 0.25);
border-radius: 14px;
padding: 10px 12px;
line-height: 1.35;
}
- Reset the counter on the list.
- Increment the counter on each list item.
- Print the counter inside
::before.
Learn more about ::before and ::after in the CSS ::before and ::after Pseudo-Elements Interactive Tutorial.
Learn more about CSS Counters in the CSS Counters Interactive Tutorial.
Text-focused pseudo-elements
::first-letter (drop caps)
::first-letter targets the first letter of the first formatted line of text.
It’s commonly used for “drop cap” effects in articles.
.article::first-letter {
font-size: 3.1em;
font-weight: 900;
float: left;
line-height: 0.95;
margin-right: 10px;
}
.article::first-letter {
font-size: 3em;
font-weight: 900;
float: left;
line-height: 0.95;
margin-right: 10px;
padding: 6px 10px;
border: 2px solid #111;
border-radius: 14px;
}
.article::first-letter {
font-size: 3.1em;
font-weight: 900;
float: left;
line-height: 0.95;
margin-right: 10px;
color: #0077b6;
text-shadow: 0 2px 0 rgba(0, 0, 0, 0.2);
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
max-width: 980px;
padding: 18px;
font-family: system-ui, Arial, sans-serif;
}
.paper {
width: min(720px, 100%);
border: 3px solid #111;
border-radius: 18px;
background: #fff;
box-shadow: 0 12px 0 #111;
padding: 16px 18px;
}
.article {
margin: 0;
line-height: 1.55;
font-size: 17px;
opacity: 0.92;
}
Pseudo-elements can target parts of text too. A drop cap is a classic example: the first letter becomes a design element without extra markup.
::first-line (style the opening line)
::first-line targets the first formatted line of a block.
The “first line” can change as the container width changes, so this effect is responsive by nature.
.article::first-line {
font-weight: 900;
}
.article::first-line {
font-weight: 900;
text-transform: uppercase;
letter-spacing: 0.06em;
}
.article::first-line {
font-weight: 900;
background: rgba(239, 35, 60, 0.18);
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
max-width: 980px;
padding: 18px;
font-family: system-ui, Arial, sans-serif;
}
.box {
width: min(720px, 100%);
border: 3px solid #111;
border-radius: 18px;
background: #fff;
box-shadow: 0 12px 0 #111;
padding: 16px 18px;
}
.article {
margin: 0;
line-height: 1.55;
font-size: 17px;
opacity: 0.92;
max-width: 62ch;
}
The first line is special. Resize your container and the “first line” changes, but the pseudo-element styling follows it. That’s why it feels a bit magical.
UI pseudo-elements you’ll recognize in the wild
::selection (style highlighted text)
::selection styles the text highlight when users select text (drag with your mouse).
Keep it readable: strong contrast is your friend.
.selectme::selection {
background: rgba(239, 35, 60, 0.35);
color: #333;
}
.selectme::selection {
background: rgba(0, 119, 182, 0.35);
color: #333;
}
.selectme::selection {
background: rgba(0, 0, 0, 1);
color: #fff;
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
max-width: 980px;
padding: 18px;
font-family: system-ui, Arial, sans-serif;
}
.box {
width: min(720px, 100%);
border: 3px solid #111;
border-radius: 18px;
background: #fff;
box-shadow: 0 12px 0 #111;
padding: 16px 18px;
}
.selectme {
margin: 0;
line-height: 1.55;
font-size: 17px;
opacity: 0.92;
}
Select this text to see
::selectionstyling. It’s small polish, but it makes your UI feel intentional.
::marker (style list bullets and numbers)
::marker targets the bullet/number of list items.
It’s great for giving lists a brand color without changing their semantics.
.list li::marker {
color: #ef233c;
}
.list li::marker {
color: #0077b6;
font-weight: 900;
}
.list li::marker {
color: #111;
font-weight: 900;
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
max-width: 980px;
padding: 18px;
font-family: system-ui, Arial, sans-serif;
}
.panel {
width: min(720px, 100%);
border: 3px solid #111;
border-radius: 18px;
background: #fff;
box-shadow: 0 12px 0 #111;
padding: 16px 18px;
}
.list {
margin: 0;
padding-left: 20px;
line-height: 1.5;
display: grid;
gap: 8px;
}
- Lists stay real lists (great for accessibility).
- You just style the marker part.
- Nice and clean.
Learn more about ::marker in the CSS List Style Interactive Tutorial.
::placeholder (style placeholder text in inputs)
::placeholder styles the placeholder text inside inputs and textareas.
Tip: don’t make placeholders too faint—people still need to read them.
.field input::placeholder {
opacity: 1;
color: rgba(0, 0, 0, 0.55);
}
.field input::placeholder {
opacity: 1;
color: rgba(0, 119, 182, 0.75);
}
.field input::placeholder {
opacity: 1;
color: rgba(239, 35, 60, 0.75);
font-style: italic;
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
max-width: 980px;
padding: 18px;
font-family: system-ui, Arial, sans-serif;
}
.field {
width: min(560px, 100%);
border: 3px solid #111;
border-radius: 18px;
background: #fff;
box-shadow: 0 12px 0 #111;
padding: 14px 16px;
display: grid;
gap: 8px;
}
.field label {
font-weight: 800;
}
.field input {
border: 2px solid #111;
border-radius: 14px;
padding: 12px 12px;
font: inherit;
outline: none;
}
.field input:focus-visible {
outline: 4px solid #0077b6;
outline-offset: 4px;
}
::file-selector-button (style upload buttons)
File inputs are famously stubborn. ::file-selector-button lets you style the button part.
.upload input::file-selector-button {
border: 2px solid #111;
border-radius: 999px;
padding: 10px 14px;
font-weight: 900;
background: #fff;
margin-right: 10px;
}
.upload input::file-selector-button {
border: 2px solid #111;
border-radius: 999px;
padding: 10px 14px;
font-weight: 900;
background: #ef233c;
margin-right: 10px;
}
.upload input::file-selector-button {
border: 2px solid #111;
border-radius: 14px;
padding: 10px 14px;
font-weight: 900;
background: #fff;
box-shadow: 0 8px 0 #111;
margin-right: 10px;
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
max-width: 980px;
padding: 18px;
font-family: system-ui, Arial, sans-serif;
}
.upload {
width: min(720px, 100%);
border: 3px solid #111;
border-radius: 18px;
background: #fff;
box-shadow: 0 12px 0 #111;
padding: 16px 18px;
display: grid;
gap: 10px;
}
.upload p {
margin: 0;
opacity: 0.85;
line-height: 1.4;
}
.upload input {
font: inherit;
}
Style the upload button (the input stays a real file input).
The ::backdrop pseudo-element (for dialogs)
When you use a native <dialog>, the browser draws a backdrop behind it.
That backdrop can be styled with ::backdrop.
See the Pen Untitled by Element How (@elementhow) on CodePen.
Accessibility rule of thumb
Pseudo-elements are best for decoration. If the text is important content (like a warning, required label, or instructions), put it in the HTML.
-
Decorative label? Great for
::before. - Real information users must read, copy, translate, or search? Put it in HTML.
-
If an overlay blocks clicks, add
pointer-events: none.
Why your pseudo-element isn’t showing up
-
You forgot
content(most common). Usecontent: "";for decorative shapes. -
You didn’t set
display, and you expected it to behave like a block. -
The parent isn’t positioned: if you’re using
position: absolute, setposition: relativeon the parent. -
It’s behind something: check
z-index, and considerisolation: isolateon the parent. -
It’s clipped:
overflow: hiddencan cut off shadows and positioned decorations. - You’re trying to add meaningful content in CSS: it might “work”, but it’s fragile and often not accessible.
Pseudo-elements cheat sheet
-
::before/::after: add decorative layers inside elements (needscontent). -
::first-letter: style the first letter (drop caps). -
::first-line: style the first formatted line. -
::selection: style selected text highlight. -
::marker: style list bullets/numbers. -
::placeholder: style placeholder text in inputs. -
::file-selector-button: style the button part of file inputs. -
::backdrop: style dialog backdrop.
CSS Pseudo Elements Conclusion
If you take only one thing away: pseudo-elements are your best friend for beautiful UI details that don’t belong in HTML. Let your markup stay clean, and let CSS do the styling work it’s good at.
