CSS Pseudo Elements: ::before, ::after and Modern Tricks
AI generated
CSS · Pseudo Elements · ::before · ::after
CSS Pseudo Elements
::before, ::after and all the modern tricks

CSS pseudo elements are one of the most powerful, yet most often superficially used, features of CSS. ::before and ::after with content and counter, the modern pseudo elements ::marker, ::selection and ::first-line: this article shows what is really possible and which tricks actually work in modern CSS.

13 min read ::before · ::after · content · counter · ::marker · ::selection All modern browsers

1. Understanding pseudo elements

CSS pseudo elements are syntactic constructs that describe specific parts of an element, or additional virtual elements that do not exist in the HTML markup. The double colon :: has distinguished them from pseudo classes like :hover or :focus since CSS3, although browsers still accept the single colon for the classic pseudo elements for compatibility reasons. The best known pseudo elements are ::before and ::after, but the range reaches far beyond that.

The distinction between pseudo classes and pseudo elements matters conceptually: pseudo classes select an element in a particular state. Pseudo elements, on the other hand, select and create a specific part of an element, or insert a new virtual element entirely. ::before creates a child element before the first child of the selected element. ::after creates one after the last child. ::first-line selects the first rendered line of text. ::marker selects the list marker. ::selection selects highlighted text.

A common misconception concerns the accessibility of pseudo elements. Content placed in the content property of ::before and ::after is visible to screen readers in most implementations, though not consistently: some browsers read out content text, others do not. The rule for anything semantically relevant is therefore simple: never place it exclusively inside pseudo elements. Decorative elements, icons via content: "", and visual aids, on the other hand, are exactly the right use case.

2. ::before and ::after: basics and limits

The basic usage pattern of ::before and ::after always requires a content property; without it, the pseudo elements are not rendered at all. content: "" is the most common value for creating an empty pseudo element that works purely visually. With position: absolute on the pseudo element and position: relative on the parent element, decorative overlays, badges, underline effects and icon placements can be implemented without any extra HTML elements.

The limits of ::before and ::after are important to know: they only work on container elements, not on replaced elements such as <img>, <input>, <br> or <hr>. The reason: replaced elements have no content of their own that anything could be prepended or appended to. A common mistake is trying to use ::before on an <img>. For icon implementations on input fields you therefore always need a wrapper element, or you use CSS backgrounds directly on the input.


/* Classic ::before / ::after patterns */

/* Decorative underline effect without extra HTML */
.link-underline {
  position: relative;
  text-decoration: none;
  color: #7c3aed;
}

.link-underline::after {
  content: "";
  position: absolute;
  bottom: -2px;
  left: 0;
  width: 0;
  height: 2px;
  background: linear-gradient(90deg, #7c3aed, #c4b5fd);
  border-radius: 1px;
  transition: width 0.3s cubic-bezier(0.22, 1, 0.36, 1);
}

.link-underline:hover::after,
.link-underline:focus-visible::after {
  width: 100%;
}

/* Badge counter on icon */
.icon-with-badge {
  position: relative;
  display: inline-flex;
}

.icon-with-badge::after {
  content: attr(data-count);
  position: absolute;
  top: -6px;
  right: -8px;
  min-width: 18px;
  height: 18px;
  padding: 0 4px;
  background: #7c3aed;
  color: #fff;
  font-size: 10px;
  font-weight: 700;
  border-radius: 9px;
  display: flex;
  align-items: center;
  justify-content: center;
  line-height: 1;
}

/* Overlay pattern for hover effect */
.card-overlay {
  position: relative;
  overflow: hidden;
}

.card-overlay::before {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(135deg, rgba(74,29,150,0.0), rgba(124,58,237,0.3));
  opacity: 0;
  transition: opacity 0.3s ease;
  pointer-events: none;
  z-index: 1;
}

.card-overlay:hover::before {
  opacity: 1;
}

3. The content property, fully utilized

The content property of pseudo elements can do far more than output a simple string. content: attr(data-label) reads an HTML data attribute and outputs it inside the pseudo element, a powerful pattern for tooltips, badges and dynamic labels without any JavaScript. content: counter(my-counter) outputs the current counter value. content: open-quote and content: close-quote insert language-specific quotation marks, configurable via quotes.

Multiple values can be combined in content with spaces: content: "§ " counter(section) ", " attr(data-title) produces a combined value made of a string, a counter and a data attribute. Since CSS Images Level 4, content also supports url() for small inline images. The alt function inside content lets you specify alternative text for screen readers: content: url(icon.svg) / "Icon description". This makes icons in pseudo elements accessible whenever they are semantically relevant.

4. CSS counters with ::before and counter()

CSS counters are one of the most frequently overlooked browser features. They allow automatic numbering of elements entirely in CSS, without JavaScript and without manually setting HTML attributes. The basic principle: counter-reset: section initializes a counter on a parent element. counter-increment: section on child elements increases the counter with every occurrence. counter(section) in the content property of a pseudo element outputs the current value.

Nested counters, typical for multi-level outlines, use counters(section, "."): this function outputs every counter in the hierarchy joined by the given separator, automatically producing numbering like "1.2.3". This is especially valuable for technical documentation, legal documents and outline views. Combined with ::before, this lets you build fully automatic numbered lists, section numbers and tables of contents that adjust themselves whenever elements are added or removed.


/* CSS Counter for automatic section numbering */
article {
  counter-reset: section;
}

article h2 {
  counter-increment: section;
}

article h2::before {
  content: counter(section) ". ";
  color: #7c3aed;
  font-variant-numeric: tabular-nums;
}

/* Nested counters for outlines */
.outline {
  counter-reset: chapter;
}

.outline .chapter {
  counter-reset: subchapter;
  counter-increment: chapter;
}

.outline .subchapter {
  counter-increment: subchapter;
}

.outline .chapter > h3::before {
  content: counter(chapter) " ";
  font-weight: 700;
  color: #4a1d96;
}

.outline .subchapter > h4::before {
  /* counters() uses all levels with separator "." */
  content: counters(chapter, ".") "." counter(subchapter) " ";
  color: #7c3aed;
}

/* Custom list style using counter */
.custom-list {
  counter-reset: list-item-custom;
  list-style: none;
  padding: 0;
}

.custom-list li {
  counter-increment: list-item-custom;
  padding-left: 2.5rem;
  position: relative;
  margin-bottom: 0.75rem;
}

.custom-list li::before {
  content: counter(list-item-custom, decimal-leading-zero);
  position: absolute;
  left: 0;
  top: 0;
  font-size: 0.75rem;
  font-weight: 700;
  color: #7c3aed;
  background: #ede9fe;
  border-radius: 4px;
  padding: 1px 5px;
  line-height: 1.6;
}

5. ::marker: styling list markers

The pseudo element ::marker selects the automatically generated list marker of <li> elements and summary elements. Before ::marker was introduced, styling list markers was extremely difficult; the usual workaround was list-style: none followed by ::before for a manual marker. With ::marker, the native list marker can be addressed directly, without bypassing the standard rendering mechanism.

The properties applicable to ::marker are deliberately limited: color, font, font-variant-numeric, content, unicode-bidi, direction, white-space and animatable properties. That is considerably less than for ::before and ::after, but it is enough for most use cases. Particularly handy: content: "" on ::marker removes the list marker entirely, without needing list-style: none, which keeps the native list structure intact for screen readers.

6. ::selection: customizing text selection

The pseudo element ::selection allows styling of text selected by the user. This is a subtle but effective design detail: the highlight color can be matched to brand colors, creating a consistent visual identity down to the smallest details. The supported properties are limited: color, background-color, text-decoration, text-shadow and caret-color. Background images and other CSS properties do not work.

An important accessibility aspect of ::selection: the contrast ratio between the selected text and the selection background must be WCAG compliant. Many designs use a brand color as the background but forget to set the text color explicitly, which can lead to poor contrast if the default text color clashes with the highlight background. Always set both values explicitly and check them with a contrast checker.


/* ::marker customization: preserves semantic list structure */
ul.styled-list {
  list-style-type: disc;
}

ul.styled-list li::marker {
  color: #7c3aed;
  font-size: 1.2em;
}

/* Custom content in marker */
ol.checkmarks li::marker {
  content: "✓ ";
  color: #059669;
  font-weight: 700;
}

/* Remove marker while keeping accessible list semantics */
nav ul li::marker {
  content: "";
}

/* ::selection: brand-consistent text highlighting */
::selection {
  background-color: #c4b5fd;
  color: #1e1b4b;
}

/* Inverted selection for dark backgrounds */
.dark-section ::selection {
  background-color: #4a1d96;
  color: #ede9fe;
}

/* ::first-line: style the first rendered line only */
.article-body p:first-of-type::first-line {
  font-variant: small-caps;
  letter-spacing: 0.04em;
  font-weight: 600;
  color: #4a1d96;
}

/* ::first-letter: drop cap effect */
.article-body p:first-of-type::first-letter {
  float: left;
  font-size: 3.5em;
  line-height: 0.85;
  margin-right: 0.08em;
  color: #7c3aed;
  font-weight: 900;
}

7. ::first-line and ::first-letter

The pseudo elements ::first-line and ::first-letter are classic typography features rooted in print tradition. ::first-line selects the first rendered line of a block element, a dynamic selection since the first line changes whenever the window is resized. Properties such as font-variant, letter-spacing, text-decoration and color can be applied to it, but layout properties such as margin or padding cannot.

::first-letter enables the classic print design feature of the drop cap: the first letter of a paragraph is rendered larger and floated to the left, so the remaining text wraps around it. ::first-letter supports full box model styling including float, padding, margin, border and font-size. One important restriction applies: the element must be a block level container, and ::first-letter only captures the very first typographic character; a leading quotation mark is not skipped but included along with it.

8. Advanced ::before/::after tricks

Some of the most powerful applications of CSS pseudo elements are less well known. The attr() pattern in content can be used for tooltips: an element with data-tooltip="Description" shows the tooltip via ::after { content: attr(data-tooltip); }, no JavaScript required, using only CSS for positioning and visibility via :hover. With anchor positioning and the popover API, this pattern can now even be positioned reliably.

The clearfix pattern, one of the historically most important pseudo element techniques, is outdated today thanks to flexbox and grid, but still instructive: .clearfix::after { content: ""; display: table; clear: both; } resolved floats without touching the HTML. What has remained modern, on the other hand, are focus ring replacement patterns using ::before, which offer better control over the visible focus ring than the native outline. By setting outline: none on the element and implementing a styled ring via ::before with position: absolute and box-shadow, you get focus indicators that fit the element exactly.

9. Pseudo elements compared

The various CSS pseudo elements have different areas of application, allowed properties and browser support. The following overview helps you pick the right pseudo element for the task at hand.

Pseudo Element Selects / Creates Key Properties Use Case
::before First virtual child element content, position, all CSS Decoration, icons, overlays
::after Last virtual child element content, position, all CSS Badges, underlines, tooltips
::marker List marker of <li> color, font, content Styled lists, custom markers
::selection Text selected by the user color, background-color Brand-consistent text highlighting
::first-line First rendered line of text font-variant, color, letter-spacing Typography, lead sentences

Combining several pseudo elements on the same element is possible. A button can use a ::before for a decorative background effect and a ::after for an animated underline at the same time. Since every element only has one ::before and one ::after, that is the maximum number of simultaneously usable generated elements per HTML element; a common refactoring step is merging several visual effects into one of these two pseudo elements.

Mironsoft

Modern CSS, clean architecture and maintainable frontends

CSS architecture and clean component patterns?

We build maintainable CSS systems with modern pseudo element patterns, clean Tailwind code and minimal JavaScript dependencies for your Hyva and Magento projects.

CSS Review

Analyze an existing CSS codebase for modern pseudo element patterns

Components

Cleanly implement buttons, links, lists and typography with ::before/::after

Tailwind v4.0

Integrate pseudo element patterns into Tailwind and Hyva themes

10. Summary

CSS pseudo elements are an indispensable tool for clean, maintainable CSS without superfluous HTML elements. ::before and ::after with the full content API, including attr(), counter() and combined values, enable decorations, automatic numbering and dynamic labels. ::marker finally gives direct access to list markers without compromising the semantic list structure. ::selection allows brand-consistent text highlighting. ::first-line and ::first-letter bring print typography to the web.

The advanced patterns, tooltip implementations with attr(), focus ring replacements, animated underlines and overlay effects, show that CSS pseudo elements go far beyond simple decoration. Combined with CSS custom properties, @property and modern layout features, they become a central part of modern CSS architectures that minimize JavaScript dependencies and let the browser do the work.

CSS Pseudo Elements: The Essentials at a Glance

::before / ::after

content: "" for empty elements. content: attr(data-x) for HTML attributes. Do not work on replaced elements (img, input).

CSS Counter

counter-reset + counter-increment + counter() in content. Nested with counters(name, ".") for multi-level numbering.

::marker

Direct styling of list markers. color, font, content allowed. content: "" removes the marker without list-style: none.

::selection

Brand text highlighting with color + background-color. Always set both values, check contrast for WCAG compliance.

11. FAQ: CSS Pseudo Elements

1Pseudo element vs. pseudo class?
Pseudo classes (:hover): states. Pseudo elements (::before): parts of the element or new virtual elements. Double colon :: for pseudo elements.
2::before on img?
Does not work: img, input and br are replaced elements without content. Use a wrapper div for overlays.
3What can content do?
Strings, attr(data-x), counter(name), open-quote, url(). Combinable. For accessible icons: url() / "Alt text".
4CSS Counter?
counter-reset on the parent element, counter-increment on the child, counter() in content. counters() for nested numbering like 1.2.3.
5::marker?
Direct styling of list markers. color, font, content allowed. content: "" removes the marker without list-style: none; screen reader semantics remain intact.
6::selection?
Style text selection. color + background-color: always set both. Check contrast for WCAG compliance.
7::first-line?
First rendered line of text: dynamic, changes with the viewport. font-variant, letter-spacing, color. No box model styling.
8Drop cap with CSS?
p::first-letter { float: left; font-size: 3.5em; line-height: 0.85; margin-right: 0.1em; }: first character enlarged, text wraps around it.
9Pseudo elements and screen readers?
Inconsistent: some browsers read out content, others do not. Do not place semantically relevant content only in pseudo elements.
10How many pseudo elements per element?
Exactly one ::before and one ::after: the maximum number of generated pseudo elements. ::marker, ::selection, ::first-line are additionally possible.