What is the difference between a pseudo-class and a pseudo-element? Give examples of each
Advertisement
🧩 Scenario
Architecture Walkthrough
A Pseudo-Class Selects a State
A pseudo-class selects elements that already exist in the DOM, based on a state or a position that cannot be expressed by a simple selector. It does not create anything; it filters.
The states come in several kinds. User interaction: :hover, :focus, :focus-visible, :focus-within, :active, :target. Form state: :checked, :disabled, :required, :valid, :invalid, :placeholder-shown, :indeterminate. Structural position: :first-child, :last-child, :nth-child(), :nth-of-type(), :only-child, :empty. Logical combinators: :not(), :is(), :where(), :has(). And link history: :link, :visited.
Written with a single colon, as a:hover.
A Pseudo-Element Selects a Sub-Part
A pseudo-element selects or creates a part of an element that is not itself in the DOM. ::before and ::after generate new boxes inside the element; ::first-line, ::first-letter, ::selection, ::marker, ::placeholder, and ::backdrop target parts the browser renders but that no element represents.
Written with a double colon, as p::first-line. The double colon was introduced in CSS3 precisely to make the distinction visible in the syntax. Browsers still accept :before, :after, :first-line, and :first-letter with one colon for backwards compatibility, but every pseudo-element added since requires two, so the double colon is the form to use consistently.
::before and ::after Need content
The most common mistake with pseudo-elements is omitting content. Without it, ::before and ::after generate nothing at all, regardless of what other properties are set. content: '' with an empty string is the standard way to create a purely decorative box, after which width, height, background, position, and transform all apply normally.
The generated box is a child of the element, positioned before or after its actual content in the layout, and it is display: inline by default, which is why width and height appear to be ignored until display: block or inline-block is also set. It also inherits from the originating element, which is convenient for colour and font but occasionally surprising.
Where Pseudo-Elements Do Not Work
::before and ::after require the element to have a content box to generate inside. Replaced elements do not, so img, input, br, hr, iframe, video, and select cannot have them. This trips people up regularly, because the CSS is valid and simply has no effect.
The usual workaround is to generate on a wrapper element instead, or to use background-image on the replaced element itself. Some browsers apply ::after to input inconsistently, which makes relying on it worse than not using it.
The Accessibility Constraint
Whether generated content reaches assistive technology is inconsistent across browsers and screen readers. Some announce it, some do not, and some do so only in certain contexts.
The practical rule is that generated content must be decorative only. An icon, a quotation mark, a divider, a tooltip arrow: all fine. A required-field asterisk that is the only indication a field is required, a status label, or the text of a badge: not fine, because a user who does not receive the generated content loses information available to everyone else.
When the content is meaningful, put it in the DOM. When it is decorative but might still be announced, content: '' / '' provides an empty alternative text string, which is the modern way to mark generated content as presentational.
Specificity
Both contribute to specificity, but to different columns. A pseudo-class counts in the class column, alongside classes and attribute selectors. A pseudo-element counts in the type column, alongside element selectors.
So a:hover is (0, 1, 1) and p::before is (0, 0, 2). Counting a pseudo-element as a class is a common error when computing tuples by hand.
The exceptions are the functional pseudo-classes: :where() contributes zero, and :is(), :not(), and :has() contribute the specificity of their most specific argument rather than a class.
Key Code Explained
/* PSEUDO-CLASSES: single colon, select existing elements by state */
a:hover { text-decoration: underline; }
input:focus-visible { outline: 2px solid #3b82f6; }
input:disabled { opacity: 0.5; }
input:checked + label { font-weight: 600; }
li:first-child { border-top: none; }
li:nth-child(odd) { background: #f8fafc; }
div:empty { display: none; }
.card:not(.card--featured) { border: 1px solid #e2e8f0; }
/* PSEUDO-ELEMENTS: double colon, target or create a sub-part */
p::first-line { font-variant: small-caps; }
p::first-letter { font-size: 3em; float: left; }
::selection { background: #bfdbfe; }
li::marker { color: #3b82f6; }
input::placeholder { color: #94a3b8; }
dialog::backdrop { background: rgb(0 0 0 / 0.5); }
/* content is REQUIRED — without it nothing renders */
.icon::before {
/* no content -> nothing appears, regardless of the rest */
width: 16px;
height: 16px;
background: url('star.svg');
}
.icon-fixed::before {
content: ''; /* the required declaration */
display: inline-block; /* pseudo-elements are inline by default */
width: 16px;
height: 16px;
background: url('star.svg');
}
/* Decorative box: the tooltip arrow pattern */
.tooltip {
position: relative;
}
.tooltip::after {
content: '';
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
border: 6px solid transparent;
border-top-color: #1e293b;
}
/* DOES NOT WORK: replaced elements have no content box */
img::before { content: ''; } /* no effect */
input::after { content: '*'; } /* unreliable at best */
/* Generate on a wrapper instead */
.field-wrapper::after { content: ''; }
/* ACCESSIBILITY: decorative only. This asterisk may not be announced. */
.required-label::after {
content: ' *';
color: #dc2626;
}
/* Better: the requirement lives in the DOM and the attribute */
/* <label>Email <span aria-hidden="true">*</span></label>
<input required aria-required="true"> */
/* Mark generated content as presentational */
.decorative::before {
content: '\2014' / ''; /* em dash with empty alt text */
}
/* SPECIFICITY: different columns */
a:hover { color: red; } /* (0, 1, 1) — pseudo-class in the CLASS column */
p::before { color: red; } /* (0, 0, 2) — pseudo-element in the TYPE column */
The .icon and .icon-fixed pair is the most useful thing here, because the broken version is what everyone writes first. Every declaration in it is valid and reasonable, and nothing renders, because content is what brings the pseudo-element into existence. The display: inline-block in the fixed version is the second half of the same lesson: even with content, an inline box ignores width and height.
The .required-label::after block is worth reading closely for the reason it is presented as a counterexample. It is extremely common in production, it looks like a clean separation of styling from markup, and it makes the required state invisible to any user whose screen reader does not announce generated content. Meaning belongs in the DOM.
Tradeoffs
| Aspect | Pseudo-class | Pseudo-element |
|---|---|---|
| Syntax | Single colon :hover | Double colon ::before |
| Selects | Existing elements in a state | A sub-part, sometimes generated |
| Creates a box | No | ::before / ::after do |
Requires content | No | Yes, for ::before / ::after |
| Works on replaced elements | Yes | No |
| Specificity column | Class | Type |
| Reliably in the accessibility tree | Yes, it is a real element | No, inconsistent |
| Examples | :hover, :checked, :nth-child(), :has() | ::before, ::first-letter, ::marker, ::backdrop |
What Interviewers Actually Check
- Whether you can state that one selects a state and the other a sub-part
- Whether you use the correct colon count and know why the double colon exists
- Whether you know
contentis required for::beforeand::after - Whether you know replaced elements cannot have pseudo-elements
- Whether you raise the accessibility constraint on generated content
Follow-Up Questions
- Why can an element have at most one
::beforeand one::afterin current CSS, and what does the multiple-pseudo-element proposal change? - What does
::markerlet you style on a list item, and what does it not let you style? - How does
::backdropwork for<dialog>and the Popover API, and where does it sit in the stacking order? - Can a pseudo-element be the target of a transition or animation? Has that always been true?
- What is the difference between
::first-lineand a wrapper span, and why can::first-lineonly accept a limited set of properties?
Common Candidate Mistakes
- Writing a single colon for a modern pseudo-element such as
:placeholderor:marker, which only accept the double-colon form - Omitting the
contentdeclaration on::beforeor::after, so the pseudo-element is never generated no matter what else is set - Applying
::beforeor::afterto animg,input, or other replaced element, where there is no content box to generate inside and the rule silently does nothing - Putting meaningful text such as a required-field indicator or a status label in generated content, which may never be announced to assistive technology
- Counting a pseudo-element in the class column when computing specificity, when it belongs in the type column alongside element selectors
Interview Readiness Checklist
Before you leave this question, make sure you can answer:
- Can you state precisely what each one selects?
- Can you name five pseudo-classes and four pseudo-elements?
- Can you explain why
contentis required on::beforeand::after, and whydisplayoften is too? - Can you name a category of element that cannot have pseudo-elements, and say why?
- Can you state which specificity column each falls into?
Summary
A pseudo-class selects elements that already exist in the DOM based on a state or position that a simple selector cannot express: :hover, :focus-visible, :checked, :disabled, :nth-child(), :has(). It filters and never creates. It is written with a single colon and counts in the class column of the specificity tuple.
A pseudo-element selects or creates a part of an element that is not itself in the DOM. ::before and ::after generate new boxes inside the element, while ::first-line, ::first-letter, ::selection, ::marker, ::placeholder, and ::backdrop target rendered parts that no element represents. It is written with a double colon, introduced in CSS3 to make the distinction visible, and counts in the type column.
Three practical constraints matter. ::before and ::after render nothing without a content declaration, and because they are inline by default, width and height also need a display change. They do not work on replaced elements such as img, input, hr, and video, because those have no content box to generate inside, so a wrapper is needed instead. And whether generated content reaches screen readers is inconsistent, so it must be decorative only: a required-field asterisk or any other meaningful text belongs in the DOM, with content: '' / '' available to explicitly mark decoration as presentational.
Why do pseudo-elements use two colons?
The double colon was introduced in CSS3 to distinguish them from pseudo-classes. Browsers still accept the single-colon form for the original four for backwards compatibility, but new pseudo-elements only support the double colon.
Is ::before content available to screen readers?
Sometimes, which is the problem. Support varies, so generated content must never carry meaning that is not also present in the DOM. Use it for decoration only.
Advertisement
Stay Updated
Get the latest frontend challenges, interview questions and tutorials delivered to your inbox.
Advertisement