What is the difference between a pseudo-class and a pseudo-element? Give examples of each

Beginner10 min interview
Skills tested:
Stating what each one selectsUsing the correct colon syntax for bothKnowing that ::before and ::after need a content propertyKnowing which elements cannot have pseudo-elementsUnderstanding the accessibility limits of generated content

Advertisement

🧩 Scenario

The distinction is not academic: it decides whether a selector can work at all. A tooltip arrow built with ::after works on a div and silently fails on an input, because replaced elements have no content box to generate into. And a required-field asterisk added with ::after content looks correct while being invisible to some screen readers, which turns a styling shortcut into an accessibility defect.

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

AspectPseudo-classPseudo-element
SyntaxSingle colon :hoverDouble colon ::before
SelectsExisting elements in a stateA sub-part, sometimes generated
Creates a boxNo::before / ::after do
Requires contentNoYes, for ::before / ::after
Works on replaced elementsYesNo
Specificity columnClassType
Reliably in the accessibility treeYes, it is a real elementNo, 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 content is required for ::before and ::after
  • Whether you know replaced elements cannot have pseudo-elements
  • Whether you raise the accessibility constraint on generated content

Follow-Up Questions

  1. Why can an element have at most one ::before and one ::after in current CSS, and what does the multiple-pseudo-element proposal change?
  2. What does ::marker let you style on a list item, and what does it not let you style?
  3. How does ::backdrop work for <dialog> and the Popover API, and where does it sit in the stacking order?
  4. Can a pseudo-element be the target of a transition or animation? Has that always been true?
  5. What is the difference between ::first-line and a wrapper span, and why can ::first-line only accept a limited set of properties?

Common Candidate Mistakes

  • Writing a single colon for a modern pseudo-element such as :placeholder or :marker, which only accept the double-colon form
  • Omitting the content declaration on ::before or ::after, so the pseudo-element is never generated no matter what else is set
  • Applying ::before or ::after to an img, 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 content is required on ::before and ::after, and why display often 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.

Frequently Asked Questions

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