What is a pseudo-element?
A pseudo-element lets you style a part of an element that has no tag of its own: the first letter of a paragraph, the bullet of a list item, the text a visitor has highlighted, or an extra box that CSS creates out of nothing. You write it as a selector followed by two colons and a name, such as p::first-line or .quote::before.
Pseudo-classes, which use one colon, select a whole element in a certain state, like :hover or :first-child. Pseudo-elements select or create a piece of an element. The double colon was introduced in CSS3 to tell the two apart. Browsers still accept a single colon for the four old ones (:before, :after, :first-line, :first-letter) so that old stylesheets keep working, but newer ones like ::marker, ::placeholder and ::backdrop only work with two. Write two colons every time and you never need to remember which is which.
Each selector can contain only one pseudo-element, and it must come at the end. .card::before:hover is invalid; write .card:hover::before to style the generated box while the card is hovered.


::before and ::after are generated inside the element, before and after its real content. They are boxes the browser draws from your CSS; they never appear in the HTML.How do ::before and ::after work?
::before creates a box as the first child of the element, and ::after creates one as the last child. They exist only when the content property has a value other than none or normal. An empty string, content: "", is the usual choice when you want a shape with no text.
The generated box is inline by default, like a <span>. That trips people up: they set a width and a height, see nothing, and assume the rule is broken. Give the box display: block or display: inline-block, or position it absolutely, and the size applies.
Because the boxes live inside the element, they inherit its font and colour, and they are clipped by overflow: hidden on it. They cannot be added to replaced elements whose content comes from outside the page, such as <img>, <input> and <video>. If you need a badge on a photo, wrap the image in a <figure> or a <div> and put the pseudo-element on the wrapper.
.price::after {
content: " per loaf";
color: #6b5a48;
}
.divider::before {
content: "";
display: block;
height: 2px;
background-color: #3b2a1a;
}What should go in content, and what should not?
The content property accepts a string, an image with url(), a counter, the value of an attribute with attr(), and the keywords open-quote and close-quote. Escapes like "\201C" produce a specific Unicode character, which avoids encoding problems in your stylesheet.
Generated text is a weak place for meaning. Screen readers differ: some read ::before text as part of the element, some skip it, and none let the user select or copy it. Translation tools may not translate it, and it disappears if the stylesheet fails to load. Keep it decorative: quote marks, arrows, separators, small icons.
When the generated content must not be announced at all, newer browsers accept alternative text after a slash: content: "\2192" / ""; tells assistive technology the arrow has an empty label. Chrome, Edge and Safari support it, and Firefox added it in 2024. When the text carries meaning, such as "New" on a product or "(opens in a new tab)" on a link, write it in the HTML, visually hidden if necessary.
.next::after {
content: "\2192" / "";
margin-left: 4px;
}How do you draw shapes and quote marks?
With an empty content, a pseudo-element is a free box for decoration. Set the parent to position: relative, the pseudo-element to position: absolute, and place it with top, left, right or bottom. Underlines that are shorter than the heading, coloured corner tabs, ribbons and dots on a timeline are all built this way, with no extra markup.
For a large opening quote mark, put the character in content, set a big font size, and give the parent enough padding so the mark does not cover the words. Using two pseudo-elements, one on each side, gives a matched pair.
Borders can make triangles: a zero-sized box with a thick top border and transparent side borders draws a downward arrow, the classic speech bubble tail. clip-path is often clearer today and is supported everywhere.
.bubble {
position: relative;
}
.bubble::after {
content: "";
position: absolute;
top: 100%;
left: 24px;
border: 10px solid transparent;
border-top-color: #f6efe4;
}How do you style list bullets with ::marker?
::marker targets the bullet or number of any element with display: list-item, which includes every <li>. It accepts a short list of properties: colour, font properties, content, and a few animation and white-space properties. Padding, margins and backgrounds do not apply.
That is enough for the common jobs. Make numbers bold and coloured, or replace the bullet with a character using content. For anything more decorative, such as a numbered circle, set list-style: none on the list and draw the marker yourself with ::before and a CSS counter. ::marker is supported in all current browsers, with Safari still limited to colour and font properties.
.steps li::marker {
color: #b5442c;
font-weight: 700;
}How do ::placeholder and ::selection work?
::placeholder styles the hint text inside an empty <input> or <textarea>. Browsers draw it in light grey by default, which often fails contrast. If you keep a placeholder, make sure its colour has at least 4.5:1 contrast with the field background, and do not use it as a replacement for a <label>: it vanishes as soon as someone types, and many screen readers treat it inconsistently.
::selection styles the text a visitor has highlighted with the mouse or keyboard. Only a few properties apply, mainly color, background-color and text decorations. Choose a pair with strong contrast; a pale highlight on pale text makes selected text unreadable.
.field::placeholder {
color: #5f5a52;
}
::selection {
background-color: #2f4a3a;
color: #fff;
}When are ::first-line and ::first-letter useful?
::first-line styles whatever text ends up on the first rendered line of a block. The amount changes with the width of the box, so on a phone it may cover four words and on a laptop twelve. It accepts font, colour and background properties, which suits a magazine-style opening with small caps.
::first-letter targets the first letter of a block, which is how you build a drop cap. Both only work on block-level containers such as paragraphs, list items and table cells. On an inline element like a <span> they do nothing. If a paragraph starts with a quote mark, the browser includes the punctuation in the first letter, which is usually what you want.
.story > p:first-of-type::first-letter {
float: left;
font-size: 3em;
line-height: 1;
margin-right: 6px;
}What is ::backdrop for?
When a <dialog> is opened with showModal(), or an element goes fullscreen, the browser draws a layer between it and the rest of the page. ::backdrop styles that layer. The default is a faint dim; you can darken it, tint it with your brand colour, or blur the page behind with backdrop-filter.
The backdrop covers the whole viewport and catches clicks, so the page behind is visibly unavailable while the dialog is open. Keep it dark enough that the dialog stands out, but not opaque, so people still see where they are. Popovers opened with the popover attribute get a ::backdrop too, though it is transparent unless you style it.
dialog::backdrop {
background-color: rgb(20 15 10 / 0.6);
}How do you inspect and debug pseudo-elements?
In browser DevTools, open the Elements panel and look inside the element: ::before and ::after appear as children with their own rules in the Styles pane. ::marker shows up inside list items. If the node is missing, the box was never generated, which nearly always means content is missing or misspelled.
When reading AI-generated CSS, look for three slips. Pseudo-elements with a width and height but no display stay invisible. Absolute pseudo-elements whose parent has no position fly to a distant corner. And meaningful words placed in content, such as a price or a warning, should move into the HTML so everyone gets them.
Field notes
- A pseudo-element styles a part of an element that has no tag of its own.
::beforeand::afteronly appear whencontenthas a value, even an empty one.- Keep
contentfor decoration; real information belongs in the HTML. - Place a decoration with
position: absoluteinside a parent set toposition: relative.
Easy to get wrong
No content means no box, and an inline box ignores width and height. Both lines are needed.
Images cannot hold generated content. Put the pseudo-element on a wrapper, and keep words like New in the HTML.
The pseudo-element must come last in the selector. The state belongs to the element.
Pale placeholder text fails contrast on a white field. Aim for 4.5:1.
Questions people ask
Can JavaScript select a pseudo-element?
No. Pseudo-elements are not in the DOM. JavaScript can read their styles with getComputedStyle(el, '::before') and change them through custom properties or classes on the element.
Why does my ::after not show on an input?
Inputs are replaced elements and cannot contain generated boxes. Wrap the input in a label or div and style the wrapper's pseudo-element.
Is one colon wrong?
For :before, :after, :first-line and :first-letter it still works. Every newer pseudo-element needs two, so use two everywhere.
How many pseudo-elements can one element have?
One ::before and one ::after, plus the part-based ones like ::marker and ::first-letter. For more decoration, add a wrapper element.