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.
::before vs ::after: which one should you use?
Functionally, ::before and ::after support the exact same properties and behave identically. The choice between them comes down to where the generated content should sit relative to the element's real content:
- Use ::before when the generated content should visually precede the element's content: icons, bullet markers, numbered badges, opening quotation marks, or a breadcrumb separator placed ahead of a label.
- Use ::after when it should follow: a required-field asterisk, an external-link arrow, a validation checkmark, closing punctuation, or an animated underline that grows beneath text on hover.
- For purely decorative content with no direction, such as a full-cover overlay or a background shape, either pseudo-element works identically. Pick one and stay consistent across your codebase so other developers know where to look.
If you need generated content on both sides at once, such as framing a heading with two decorative lines, you are not limited to picking just one. See using both ::before and ::after together further down this guide.
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; }
7. Before/after image comparison slider
This is one of the most common places ::after shows up outside of pure decoration: the draggable divider line on a before/after image comparison widget. The two images are stacked, the top image is clipped to a percentage width, and a range input drives that percentage through a CSS custom property. The ::after pseudo-element becomes the visible slider handle line, so no extra markup is needed for it.
/* HTML structure */ /* <div class="compare"> <img class="compare-after" src="after.jpg" alt="After"> <div class="compare-before"> <img src="before.jpg" alt="Before"> </div> <input type="range" min="0" max="100" value="50" class="compare-range"> </div> */ .compare { position: relative; overflow: hidden; --pos: 50%; } .compare-after, .compare-before img { display: block; width: 100%; } .compare-before { position: absolute; top: 0; left: 0; height: 100%; overflow: hidden; width: var(--pos); } .compare-before::after { content: ""; position: absolute; top: 0; right: 0; width: 3px; height: 100%; background: #fff; box-shadow: 0 0 6px rgba(0, 0, 0, 0.5); } .compare-range { position: absolute; inset: 0; width: 100%; opacity: 0; cursor: ew-resize; }
// One line of JS keeps --pos in sync with the range input // document.querySelector('.compare-range').addEventListener('input', e => { // e.target.closest('.compare').style.setProperty('--pos', e.target.value + '%'); // });
The ::after here does one job only: draw the thin handle line at the current clip edge. Everything else, clipping the image and reading the slider position, is handled by the custom property and the wrapping elements. This keeps the pseudo-element usage purely decorative, which matters for accessibility (see below).
8. Reserve space for bold text on hover
Switching font-weight to bold on :hover is a common micro-interaction on nav links and buttons, but bold characters are wider than regular ones, so the element (and anything next to it) visibly shifts when the text becomes bold. A zero-height ::after that pre-renders the bold version of the text solves this: it reserves the extra width in advance without taking up any vertical space or being visible.
/* HTML: <a class="nav-link" data-text="Pricing" href="/pricing/">Pricing</a> */ .nav-link { display: inline-block; font-weight: 400; position: relative; } .nav-link::after { content: attr(data-text); display: block; height: 0; overflow: hidden; visibility: hidden; font-weight: 700; white-space: nowrap; } .nav-link:hover { font-weight: 700; }
Because .nav-link is inline-block, its width is normally determined by its visible content. Adding a hidden, bold, zero-height copy of the same text through ::after forces the browser to size the element wide enough for the bold version up front, so hovering only changes weight, not layout.
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. This is why you cannot add a::beforeto an<img>tag directly; if you need an overlay on an image, wrap it in a<div>or<figure>and apply the pseudo-element to that wrapper instead. - 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, and support varies significantly between browsers and assistive technology. Never place essential information inside::beforeor::after; treat generated content as decorative only, and keep anything a user needs to read or act on in real HTML. - 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.
Troubleshooting common ::before and ::after issues
The pseudo-element jumps to the top of the page
An absolutely positioned pseudo-element positions itself relative to the nearest ancestor that has position set to anything other than static. If no ancestor qualifies, it falls back to the initial containing block, which places it near the top-left of the whole document. Fix it by adding position: relative to the direct parent, not a distant ancestor.
/* Missing position: relative on the parent causes this */ .card { position: relative; /* add this line */ } .card::before { position: absolute; top: 0; left: 0; }
The pseudo-element is clipped or invisible
If a parent has overflow: hidden, overflow: auto, or overflow: scroll, any part of a positioned pseudo-element that extends past the parent's box gets cut off. This is by design (it is the same overflow rule that applies to any child element) but it is easy to forget when debugging a pseudo-element that "disappears" after adding a border-radius or a clipping wrapper.
A background or another element covers the pseudo-element
::before and ::after are painted in the same stacking order as a real first or last child: above the parent's own background and border, but below any sibling or child that has a higher stacking context. If another element visually sits on top of your pseudo-element, give the pseudo-element position: relative or position: absolute together with a z-index higher than the element covering it.
The pseudo-element doesn't match its parent's height
An absolutely positioned pseudo-element does not automatically stretch to fill its container. Set an explicit height: 100%, or use inset: 0 (shorthand for top: 0; right: 0; bottom: 0; left: 0;) if you want it to cover the parent completely.
Adding a space or extra character after text
Because content takes a quoted string, you can insert literal spaces or special characters directly inside the quotes, including a non-breaking space so it survives whitespace collapsing:
.label::after { content: "\00a0→"; /* non-breaking space, then an arrow */ }
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.
Using ::before and ::after in Tailwind CSS
Tailwind CSS supports pseudo-elements through the before: and after: variant prefixes, which apply any utility class to the generated ::before or ::after element. The content property is still mandatory in plain CSS, so always pair a before: or after: utility with a content-[] value, even for decorative pseudo-elements that carry no text.
<!-- Text content --> <span class="before:content-['→'] before:mr-2">Next</span> <!-- Decorative, no visible text: content-[''] is required to render --> <div class="before:content-[''] before:block before:h-px before:w-full before:bg-gray-300"></div>
Use content-[attr(...)] to read a value from an HTML attribute, the same way attr() works in plain CSS:
<div data-label="Required" class="after:content-[attr(data-label)] after:ml-1 after:text-red-500"> Email </div>
Because whitespace ends a Tailwind class, replace spaces inside an arbitrary content value with an underscore, for example content-['Hello_World']. In Tailwind v4, you can also reference a CSS custom property directly with content-(--my-variable), which is shorthand for content-[var(--my-variable)], useful for pseudo-elements whose content changes dynamically via JavaScript.
How ::before and ::after relate to other pseudo-elements
::before and ::after belong to a small group of pseudo-elements that generate new, styleable content. Most other CSS pseudo-elements work the opposite way: they style a part of content that already exists, and none of them use the content property.
| Pseudo-element | What it does | Requires content? |
|---|---|---|
| ::before / ::after | Inserts a new generated element before or after existing content | Yes |
| ::selection | Styles the portion of text a user has highlighted | No |
| ::placeholder | Styles placeholder text inside an input or textarea | No |
| ::marker | Styles the bullet or number of a list item | No |
| ::first-line | Styles the first rendered line of a block of text | No |
| ::first-letter | Styles the first letter of a block of text | No |
In practice, this means ::before and ::after cover a different job than the rest: they are for adding something that was not there, while ::selection, ::placeholder, ::marker, and the others are for restyling something that already is.
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.
Should I use ::before or ::after to add content?
It depends on where the generated content should sit relative to the element's real content. Use ::before when it should visually precede the content, such as icons, bullet markers, or opening quotation marks. Use ::after when it should follow, such as badges, arrows, or closing punctuation. For purely decorative content with no direction, either works identically, so pick one and stay consistent.
Can I use ::before and ::after with Tailwind CSS?
Yes. Tailwind applies utility classes to pseudo-elements using the before: and after: variant prefixes. The content property is still required, so pair every before: or after: utility with a content-[] value, such as content-[''] for decorative elements.
Why does my ::before or ::after pseudo-element jump to the top of the page?
This happens when the pseudo-element uses position: absolute but its parent has no positioning context. Add position: relative to the direct parent to fix it.
Why is my ::before or ::after pseudo-element hidden behind the background or other elements?
::before and ::after paint above the parent's own background but below any positioned sibling or child with a higher stacking order. Give the pseudo-element position plus a z-index value higher than the element covering it.
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.