Articles

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

How ::before and ::after generate content without extra markup, which properties they need, and common patterns like icons and counters.

Takina Takina · · 5 min read
A close-up of CSS code on a screen

::before and ::after are CSS pseudo-elements that insert generated content as the first or last child of an element, without adding any extra markup to the HTML. They’re how CSS handles decorative content, icons, and small bits of text that don’t belong in the document itself.

Generated content needs the content property

A pseudo-element only renders if it has a content property — even an empty string counts:

.tooltip::after {
  content: "";
  position: absolute;
  /* triangle, border, background, etc. */
}

Without content, ::before and ::after produce nothing, no matter what other properties you set. The value can be a literal string, a quoted attribute reference (content: attr(data-label)), a counter, or an image via url().

.required::after {
  content: " *";
  color: red;
}

Both pseudo-elements are inline by default, so they typically need display: block or display: inline-block before properties like width, height, or position take effect the way you’d expect.

What they are, structurally

::before and ::after behave like real children of their element for styling purposes — they participate in layout, can be positioned, and inherit from the parent — but they aren’t part of the DOM. You can’t select them with document.querySelector, and they aren’t visible to assistive technology in a form screen readers can interact with (though their text content is sometimes announced, which is why purely decorative content should stay out of content strings).

That distinction matters: ::before and ::after are pseudo-elements, generating actual boxes in the render tree, which is different from pseudo-classes like :hover or :nth-child(), which just match existing elements based on state or position. If you haven’t already, the pieces fit together well alongside structural pseudo-classes like :nth-child and the :is() and :where() selectors — pseudo-classes select, pseudo-elements generate.

Common use cases

Icons and decorative glyphs. Adding a small icon before a link or list item without an extra <span>:

.external-link::after {
  content: "↗";
  margin-left: 0.25em;
}

Clearfix. Before flexbox and grid were reliable, ::after was the standard fix for a container collapsing around floated children:

.clearfix::after {
  content: "";
  display: table;
  clear: both;
}

Numbered lists with custom formatting. Paired with CSS counters, ::before can generate structured numbering that plain <ol> markup can’t express, like “Section 2.3”:

.section {
  counter-increment: section;
}
.section::before {
  content: "Section " counter(section) ": ";
}

See our guide to CSS counters for the full counter-reset / counter-increment mechanics.

Tooltips and speech bubbles. A common pattern uses ::before for the bubble body and ::after for the pointer triangle, both absolutely positioned relative to the parent — two extra boxes for the price of zero extra HTML elements.

Quote marks. Browsers apply typographic quotation marks to <q> elements via ::before/::after and the quotes property, a good example of the browser’s own stylesheet using the same mechanism you can use yourself.

What they can’t do

Generated content has real limits. content strings aren’t selectable or copyable as normal text in most browsers, so they shouldn’t carry information users need to copy — like a price or a code. They also can’t contain arbitrary HTML; content only accepts strings, counters, attr() values, and a handful of image and quote functions, not markup with its own tags.

Each element can only have one ::before and one ::after — you can’t stack multiple generated pseudo-elements on the same selector the way you can layer multiple real children. If you need more than two generated boxes, you’re back to adding actual elements or building the pattern with ::before/::after combined with CSS masking for more complex shapes.

Styling them like real elements

Because ::before and ::after generate boxes, most standard properties apply: background, border, transform, transition, even animation. They also respect custom properties inherited from their parent, which is useful for theming a decorative element without duplicating values:

.badge {
  --badge-color: crimson;
}
.badge::before {
  content: "";
  background: var(--badge-color);
}

One subtlety: pseudo-elements defined in different cascade layers follow the same layer-ordering rules as any other selector, so a ::before rule in a later layer can still override one in an earlier layer regardless of specificity.

Debugging generated content

Because ::before and ::after aren’t in the DOM, document.querySelector and similar JavaScript APIs can’t find them, and they won’t show up in a plain “view source.” Browser developer tools do render them in the element inspector, usually as a labeled ::before or ::after pseudo-node nested under their parent, which is the reliable way to check whether a pseudo-element is actually generating content, what its computed styles are, and whether a missing content declaration is the reason nothing is showing up.

A common gotcha: setting content: none (rather than an empty string) explicitly suppresses generation, which is useful for undoing a ::before rule inherited from a more general selector without having to override every other property it set. This is a cleaner override than trying to reset display: none, since content: none matches the same mechanism that controls whether the pseudo-element renders at all.

Single-colon vs double-colon syntax

Older CSS allowed a single colon for pseudo-elements — :before and :after — a holdover from CSS2, when pseudo-elements and pseudo-classes shared the same syntax. CSS3 introduced the double-colon form specifically to distinguish pseudo-elements (which generate a new box) from pseudo-classes (which just match existing elements based on state). Browsers still accept the single-colon form for :before and :after for backward compatibility, but new code should use the double-colon form — it’s the current standard, and it makes the distinction from pseudo-classes explicit at a glance.

The takeaway

::before and ::after insert generated, non-DOM content as an element’s first or last child, and they only render once content is set. They’re the right tool for decorative glyphs, layout hooks like clearfix, and structured counters — but not for content users need to select, copy, or have announced reliably by assistive technology. Treat them as presentation, keep real content in the markup, and reach for them when you’d otherwise be adding an empty <span> just to hang styles on.

Takina Takina · · 4 min read

CSS object-fit and object-position, Explained

object-fit controls how an image or video is cropped inside its box, and object-position controls which part of it stays visible. How they work together.

#CSS #Web Development #Frontend
Takina Takina · · 5 min read

CSS inherit, initial, unset & revert Explained

CSS's four global keywords control where a property's value comes from. How inherit, initial, unset, and revert differ, with a comparison table.

#CSS #Web Development #Frontend
Takina Takina · · 4 min read

Dynamic Viewport Units: dvh, svh, and lvh Explained

dvh, svh, and lvh fix the classic mobile vh bug where browser toolbars cut off full-height layouts. Here's what each unit measures and when to use it.

#CSS #Web Development #Frontend