CSSPseudo-Element Patterns

Pseudo-Element Patterns

::before and ::after are two of the most useful tools in CSS. They inject generated content into an element without touching the HTML, which makes them perfect for decoration, icons, counters, and small UI flourishes that shouldn't clutter your markup. This page is a roundup of the recipes you'll reach for again and again — decorative icons and badges, tooltip arrows, numbered counters, and "required field" indicators.

Note
Every pattern here relies on the fact that ::before and ::after generate a real, styleable box that sits inside the element's content — before or after the actual content — and requires a content property (even an empty string) to render at all.
Decorative icons and badges

A common use case is adding a small decorative icon or badge next to text without adding an extra HTML element. Because the content is purely decorative, it should be hidden from assistive technology — which pseudo-elements already do implicitly, since they aren't exposed in the accessibility tree as separate nodes with alt text.

CSS
/* External link icon */
a[href^="http"]::after {
  content: "↗";
  display: inline-block;
  margin-left: 0.25em;
  font-size: 0.85em;
  color: currentColor;
}

/* "New" badge on a nav item */
.nav-item.is-new::after {
  content: "NEW";
  margin-left: 0.5em;
  padding: 0.1em 0.5em;
  font-size: 0.7em;
  font-weight: 700;
  letter-spacing: 0.04em;
  color: white;
  background: #e63946;
  border-radius: 999px;
  vertical-align: middle;
}

/* Checklist item with a checkmark bullet */
.checklist li::before {
  content: "✓";
  display: inline-block;
  width: 1.25em;
  margin-right: 0.4em;
  color: #2a9d8f;
  font-weight: 700;
}
Tooltip arrows

A classic tooltip is just a positioned box with a small triangular "arrow" pointing at the element it describes. The arrow itself is usually a zero-size pseudo-element with only borders set — three transparent, one colored — rotated or positioned to form a triangle pointing in the right direction.

CSS
.tooltip {
  position: relative;
  display: inline-block;
  padding: 0.5em 0.75em;
  background: #1a1a1a;
  color: white;
  border-radius: 6px;
  font-size: 0.875rem;
}

/* Arrow pointing downward at the trigger element */
.tooltip::after {
  content: "";
  position: absolute;
  bottom: -6px;
  left: 50%;
  width: 12px;
  height: 12px;
  background: #1a1a1a;
  transform: translateX(-50%) rotate(45deg);
  /* A rotated square, half-hidden below the box, reads as an arrow */
}

/* Alternative: pure border-triangle arrow, no rotation needed */
.tooltip--border-arrow::after {
  content: "";
  position: absolute;
  top: 100%;
  left: 50%;
  transform: translateX(-50%);
  border: 6px solid transparent;
  border-top-color: #1a1a1a;
}
Numbered step counters

Combine ::before with CSS counters to automatically number a list of steps or sections without hardcoding numbers in the markup — the numbers stay correct even if you reorder or add steps. See @counter-style for how far you can push custom counter formatting (roman numerals, custom glyphs, and more).

CSS
.steps {
  list-style: none;
  counter-reset: step-counter;
  padding: 0;
}

.steps li {
  counter-increment: step-counter;
  position: relative;
  padding-left: 2.5em;
  margin-bottom: 1em;
}

.steps li::before {
  content: counter(step-counter);
  position: absolute;
  left: 0;
  top: 0;
  width: 1.75em;
  height: 1.75em;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #264653;
  color: white;
  border-radius: 50%;
  font-weight: 700;
  font-size: 0.9em;
}
Required-field asterisk
A required form field is often marked with a small asterisk after its label. Generating it with a pseudo-element keeps the asterisk purely visual while the actual "required" semantics live on the input itself (via the required attribute and aria-required where needed) — so screen reader users still get the required state announced correctly, independent of whatever CSS renders on screen.

CSS
label.required::after {
  content: " *";
  color: #d62828;
  font-weight: 700;
}

/* Or scope it to a dedicated <span> for clearer intent */
.required-mark::after {
  content: "*";
  color: #d62828;
  margin-left: 0.2em;
}
Warning
Never rely on generated content alone to convey required-ness or any other meaningful state. An asterisk added purely via CSS content is invisible to some assistive technology configurations and disappears entirely if CSS fails to load. Always pair it with the real required attribute (and ideally visible text like "(required)" for the clearest experience).
Worked example: combining patterns

A card component that uses a numbered badge, a decorative corner ribbon, and a required-field style all at once shows how naturally these patterns compose.

CSS
<!-- HTML -->
<div class="feature-card featured">
  <h3>Pro Plan</h3>
  <label class="required">Card number</label>
  <input type="text" required />
</div>

/* CSS */
.feature-card {
  position: relative;
  padding: 1.5rem;
  border: 1px solid #ddd;
  border-radius: 8px;
  overflow: hidden;
}

.feature-card.featured::before {
  content: "Featured";
  position: absolute;
  top: 0.75rem;
  right: -2.5rem;
  padding: 0.25rem 3rem;
  background: #e9c46a;
  color: #1a1a1a;
  font-size: 0.75rem;
  font-weight: 700;
  transform: rotate(45deg);
}

.feature-card label.required::after {
  content: " *";
  color: #d62828;
}
Next
Now that you know the recipes, revisit the fundamentals any time with [::before & ::after](/css/before-after), and see how far generated counters can go with [@counter-style](/css/at-counter-style).