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-elementscontent: "text"— inserts a literal text stringcontent: attr(data-label)— reads a value from an HTML attributecontent: counter(section)— inserts a CSS counter valuecontent: 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
- Void elements have no pseudo-elements. Tags like
<img>,<input>,<br>, and<hr>cannot have children — so::beforeand::afterdo not work on them. - Not selectable by JavaScript. You cannot use
querySelector("::before"). If you need to interact with pseudo-elements via JS, toggle a class on the parent or use CSS custom properties to pass values in. - Not accessible. Screen readers may or may not read
contentvalues. Never use::beforeor::afterto insert meaningful text — 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. Use both together, or nest an extra element.
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: #2d3148; }
This creates a horizontal rule on both sides of a heading with no extra HTML elements needed.
Tip: Use ::before for decorative elements before content and ::after for elements after. This maintains semantic meaning even though they are purely visual.
FAQ
Do ::before and ::after work on all HTML elements?
No. Void elements — tags that cannot have children such as <img>, <input>, <br>, and <hr> — do not support pseudo-elements. For all other HTML elements, ::before and ::after work normally.
Why is my ::before or ::after not showing?
The most common reason is a missing content property. Without it, even an empty string, the browser discards the pseudo-element entirely. The second common reason is display — pseudo-elements are inline by default, so width and height have no effect until you add display: block or display: inline-block.
Can I have multiple ::before pseudo-elements on one element?
No. Each element supports only one ::before and one ::after. To work around this, use both together on the same element, or nest an additional element and apply pseudo-elements to that.
Can ::before and ::after be targeted with JavaScript?
querySelector("::before") does not work — pseudo-elements are not part of the DOM. To interact with them from JavaScript, toggle a class on the parent and write CSS rules for each class state, or pass values through CSS custom properties using element.style.setProperty('--my-var', value).
Can I animate ::before and ::after pseudo-elements?
Yes. CSS transitions and @keyframes animations work on pseudo-elements the same way they work on regular elements. A common pattern is animating the width of an ::after underline on hover using transition: width 0.3s ease.
What is the difference between :before and ::before?
They produce the same result. The single-colon :before syntax is from CSS2 and still works in all browsers. The double-colon ::before was introduced in CSS3 to distinguish pseudo-elements from pseudo-classes. Use ::before in all new code.
Build animations for pseudo-element effects
Combine ::before and ::after with keyframe animations using our CSS Animation Generator — build and preview effects in real time.