CSS ::before and ::after are pseudo-elements that let you insert content before or after an element's actual content — without touching your HTML. They are one of the most powerful tools in CSS, used for decorative effects, custom icons, overlays, counters, and layout tricks. This guide explains exactly how they work and how to use them effectively.

What are ::before and ::after?

A pseudo-element is a keyword added to a selector that lets you style a specific part of an element — or in the case of ::before and ::after, insert a virtual child element inside it. These inserted elements exist only in CSS. They are not part of your HTML source and cannot be selected by JavaScript using standard DOM methods.

::before inserts a generated element as the first child of the selected element. ::after inserts one as the last child. Both elements are inline by default and require the content property to render — even if that content is empty.

.element::before {
  content: "→ ";
}

.element::after {
  content: " ←";
}

Note: You may see both single-colon (:before) and double-colon (::before) syntax. The double colon was introduced in CSS3 to distinguish pseudo-elements from pseudo-classes. Both work in modern browsers, but ::before is the correct modern syntax.

The content property — the one rule you cannot skip

The content property is mandatory. Without it, ::before and ::after will not render, even if you give them a width, height, and background color. The browser simply ignores the pseudo-element entirely if content is missing.

/* This will NOT render — no content property */
.element::before {
  width: 20px;
  height: 20px;
  background: red;
}

/* This WILL render — content is present (even if empty) */
.element::before {
  content: "";
  display: block;
  width: 20px;
  height: 20px;
  background: red;
}

The content property accepts several value types:

  • content: "" — empty string, used for purely decorative pseudo-elements
  • content: "text" — inserts a literal text string
  • content: attr(data-label) — reads a value from an HTML attribute
  • content: counter(section) — inserts a CSS counter value
  • content: url("/img/icon.svg") — inserts an image (limited control over sizing)

Display and positioning

By default, ::before and ::after are inline elements. This means they flow with text and do not respond to width or height. To use them as blocks, shapes, or positioned overlays, you need to change their display type.

.element::before {
  content: "";
  display: block;        /* takes full width, stacks vertically */
  /* or */
  display: inline-block; /* respects width/height, stays inline */
  /* or */
  display: flex;         /* makes pseudo-element a flex container */
}

For overlays and absolutely positioned decorations, combine position: absolute on the pseudo-element with position: relative on the parent:

.card {
  position: relative; /* required for absolute pseudo-element */
}

.card::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(0, 0, 0, 0.4);
}

Practical examples

1. Decorative quotation marks

blockquote {
  position: relative;
  padding-left: 2rem;
  font-style: italic;
}

blockquote::before {
  content: "\201C"; /* opening double quote */
  position: absolute;
  left: 0;
  top: -0.25rem;
  font-size: 3rem;
  color: #7c6af7;
  line-height: 1;
}

2. Animated underline on links

.link {
  position: relative;
  text-decoration: none;
}

.link::after {
  content: "";
  position: absolute;
  bottom: -2px;
  left: 0;
  width: 0;
  height: 2px;
  background-color: #7c6af7;
  transition: width 0.3s ease;
}

.link:hover::after {
  width: 100%;
}

3. Custom checkbox style

.checkbox {
  position: relative;
  padding-left: 1.75rem;
  cursor: pointer;
}

.checkbox input {
  display: none;
}

.checkbox::before {
  content: "";
  position: absolute;
  left: 0;
  top: 2px;
  width: 16px;
  height: 16px;
  border: 2px solid #7c6af7;
  border-radius: 3px;
  background: #fff;
}

.checkbox input:checked + .checkbox::after {
  content: "✓";
  position: absolute;
  left: 3px;
  top: 0;
  color: #7c6af7;
  font-weight: bold;
}

4. Ribbon / badge effect

.card {
  position: relative;
  overflow: hidden;
}

.card::before {
  content: "NEW";
  position: absolute;
  top: 12px;
  right: -24px;
  background: #ef4444;
  color: #fff;
  font-size: 0.7rem;
  font-weight: bold;
  padding: 4px 28px;
  transform: rotate(45deg);
  letter-spacing: 0.05em;
}

5. Reading attr() data from HTML

/* HTML: <button data-tooltip="Save your file">Save</button> */

.tooltip-btn {
  position: relative;
}

.tooltip-btn::after {
  content: attr(data-tooltip);
  position: absolute;
  bottom: 110%;
  left: 50%;
  transform: translateX(-50%);
  background: #1e293b;
  color: #fff;
  padding: 0.4rem 0.75rem;
  border-radius: 4px;
  font-size: 0.8rem;
  white-space: nowrap;
  opacity: 0;
  transition: opacity 0.2s ease;
}

.tooltip-btn:hover::after {
  opacity: 1;
}

6. CSS counters with ::before

ol.custom-list {
  list-style: none;
  counter-reset: list-counter;
  padding: 0;
}

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

ol.custom-list li::before {
  content: counter(list-counter);
  position: absolute;
  left: 0;
  top: 0;
  width: 1.75rem;
  height: 1.75rem;
  background: #7c6af7;
  color: #fff;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 0.8rem;
  font-weight: bold;
}

What ::before and ::after cannot do

There are a few important limitations to be aware of:

  • Void elements have no pseudo-elements. Tags like <img>, <input>, <br>, and <hr> cannot have children — so ::before and ::after do not work on them.
  • Not selectable by JavaScript. You cannot use querySelector("::before"). If you need to interact with pseudo-elements via JS, you either toggle a class on the parent or use CSS custom properties to pass values in.
  • Not accessible. Screen readers may or may not read content values. Never use ::before or ::after to insert meaningful text that users need to understand — only use them for purely decorative content.
  • Only one ::before and one ::after per element. You cannot stack multiple pseudo-elements of the same type on a single selector. To work around this, nest elements or use both ::before and ::after together.

Using both ::before and ::after together

You can use both on the same element at once, giving each element effectively two free "bonus" child elements. A common use case is decorative dividers or framed headings:

.section-title {
  display: flex;
  align-items: center;
  gap: 1rem;
  text-align: center;
}

.section-title::before,
.section-title::after {
  content: "";
  flex: 1;
  height: 1px;
  background-color: #e2e8f0;
}

This creates a horizontal rule on both sides of a heading — with no extra HTML elements needed.

Pro tip: Use ::before for decorative elements before content and ::after for elements after. This maintains semantic meaning even though they're purely visual.

Create beautiful pseudo-element effects

Combine ::before and ::after with animations and transitions for truly dynamic effects. Our CSS tools can help you build these faster.

Open CSS Animation Generator →