Pseudo-Classes vs Pseudo-Elements
The distinction between pseudo-classes and pseudo-elements is one of the most commonly confused CSS topics. They use similar syntax, both extend selector capabilities, but they serve fundamentally different purposes.
The Core Distinction
Pseudo-class: Selects an EXISTING element in a particular STATE
Pseudo-element: Creates a VIRTUAL element that doesn't exist in the DOM| Feature | Pseudo-Class | Pseudo-Element |
|---|---|---|
| Syntax | Single colon : | Double colon :: |
| Purpose | Select by state/position | Create virtual content |
| DOM element | Already exists | Doesn't exist in DOM |
| Quantity | Unlimited per selector | Limited (few per element) |
| Example | :hover, :first-child | ::before, ::after |
Pseudo-Classes
User Action States
a:hover { color: blue; } /* Mouse is over element */
a:active { color: red; } /* Element is being clicked */
a:focus { outline: 2px solid; } /* Element has keyboard focus */
a:focus-visible { outline: 2px solid blue; } /* Focus via keyboard only */
a:focus-within { border-color: blue; } /* A descendant has focus */
a:visited { color: purple; } /* Link has been visited */
a:link { color: blue; } /* Unvisited link */:focus vs :focus-visible: :focus fires on all focus (including mouse clicks). :focus-visible fires only when focus should be visually indicated (keyboard navigation). Use :focus-visible for outlines to avoid showing outlines on mouse clicks:
button:focus { outline: none; } /* Remove default */
button:focus-visible { outline: 2px solid blue; } /* Show for keyboard */Structural Pseudo-Classes
li:first-child { } /* First child of its parent */
li:last-child { } /* Last child of its parent */
li:nth-child(2) { } /* Second child */
li:nth-child(odd) { } /* 1st, 3rd, 5th... */
li:nth-child(even) { } /* 2nd, 4th, 6th... */
li:nth-child(3n) { } /* Every 3rd (3, 6, 9...) */
li:nth-child(3n+1) { } /* 1st of every 3 (1, 4, 7...) */
li:nth-last-child(2) { } /* Second from last */
li:only-child { } /* Only child of its parent */
p:first-of-type { } /* First <p> among siblings */
p:last-of-type { } /* Last <p> among siblings */
p:nth-of-type(2) { } /* Second <p> among siblings */
p:only-of-type { } /* Only <p> among siblings */:nth-child vs :nth-of-type:
<div>
<h2>Title</h2> <!-- h2:first-child, h2:first-of-type -->
<p>First para</p> <!-- p:nth-child(2), p:first-of-type -->
<p>Second para</p> <!-- p:nth-child(3), p:nth-of-type(2) -->
</div>:nth-child(2) selects the second child regardless of type. :nth-of-type(2) selects the second element of that specific type.
Form State Pseudo-Classes
input:required { border-left: 3px solid blue; }
input:optional { border-left: 3px solid gray; }
input:valid { border-color: green; }
input:invalid { border-color: red; }
input:disabled { opacity: 0.5; cursor: not-allowed; }
input:enabled { }
input:checked { }
input:placeholder-shown { }
input:read-only { background: #f5f5f5; }
input:in-range { } /* Number input within min/max */
input:out-of-range { color: red; }
input:autofill { box-shadow: 0 0 0 1000px white inset; }Modern Pseudo-Classes
/* :is() â matches any argument, takes highest specificity */
:is(h1, h2, h3):hover { color: blue; }
/* :where() â same as :is() but ZERO specificity */
:where(h1, h2, h3) { margin-bottom: 1rem; }
/* :has() â parent selector (selects element based on descendants) */
.card:has(img) { grid-template-rows: 200px 1fr; }
.card:has(:invalid) { border-color: red; }
form:has(:focus-within) { box-shadow: 0 0 0 2px blue; }
/* :not() â negation */
input:not(:disabled):not(:read-only) { cursor: text; }
/* :empty â element with no children (not even whitespace in modern browsers) */
.message:empty { display: none; }Pseudo-Elements
::before and ::after
Create virtual elements as the first/last child:
.required-field::after {
content: ' *';
color: red;
}
.external-link::after {
content: ' â';
font-size: 0.75em;
}
.quote::before {
content: open-quote;
}
.quote::after {
content: close-quote;
}Critical rule: ::before and ::after require the content property (even if empty):
/* Won't render â missing content */
.element::before {
width: 20px;
height: 20px;
background: red;
}
/* Will render */
.element::before {
content: '';
display: block;
width: 20px;
height: 20px;
background: red;
}Common ::before / ::after Patterns
/* Decorative line */
.section-title::after {
content: '';
display: block;
width: 60px;
height: 3px;
background: var(--color-primary);
margin-top: 0.5rem;
}
/* Overlay on images */
.card-image::after {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(transparent 50%, rgba(0,0,0,0.8));
}
/* Custom checkbox */
.checkbox::before {
content: '';
width: 20px;
height: 20px;
border: 2px solid #ccc;
border-radius: 4px;
}
.checkbox:checked::before {
background: var(--color-primary);
border-color: var(--color-primary);
}
/* Counter */
ol {
counter-reset: step;
}
li::before {
counter-increment: step;
content: counter(step) '. ';
font-weight: bold;
color: var(--color-primary);
}Other Pseudo-Elements
/* First line of a block */
p::first-line {
font-weight: bold;
text-transform: uppercase;
}
/* First letter (drop cap) */
p::first-letter {
font-size: 3em;
float: left;
line-height: 1;
margin-right: 0.1em;
}
/* Text selection highlight */
::selection {
background: var(--color-primary);
color: white;
}
/* Placeholder text */
input::placeholder {
color: #999;
font-style: italic;
}
/* Scrollbar styling (Webkit) */
::-webkit-scrollbar { width: 8px; }
::-webkit-scrollbar-track { background: #f1f1f1; }
::-webkit-scrollbar-thumb { background: #888; border-radius: 4px; }
/* List marker */
li::marker {
color: var(--color-primary);
font-size: 1.2em;
}
/* File input button */
input[type="file"]::file-selector-button {
padding: 8px 16px;
border-radius: 4px;
background: var(--color-primary);
color: white;
}Specificity Comparison
:hover /* (0,0,1,0) â one pseudo-class */
::before /* (0,0,0,1) â one pseudo-element */
:first-child /* (0,0,1,0) â one pseudo-class */
::first-line /* (0,0,0,1) â one pseudo-element */
:is(.a, #b) /* (0,1,0,0) â takes highest argument (the #b) */
:where(.a, #b) /* (0,0,0,0) â always zero */
:not(.active) /* (0,0,1,0) â specificity of argument */
:has(.checked) /* (0,0,1,0) â specificity of argument */Pseudo-classes contribute to the class/attribute column. Pseudo-elements contribute to the element column.
Accessibility Note
::before and ::after content:
- Is read by most screen readers (but inconsistently)
- Should not contain essential information
- Use
content: '' / ''for decorative elements - For critical content, use real DOM elements with appropriate ARIA
/* Decorative â screen reader should ignore */
.icon::before {
content: 'â
';
/* Add aria-hidden on the parent or use an actual <span aria-hidden="true"> */
}Interview Signal
Senior candidates demonstrate:
- Clear distinction â Pseudo-classes select by state, pseudo-elements create virtual nodes; single vs double colon
:nth-childformulas â Can explainan+bsyntax, knows the difference between:nth-childand:nth-of-type:focus-visibleâ Knows why it exists (keyboard vs mouse focus indication)::before/::aftermastery â Requirescontent, common patterns (decorative lines, overlays, counters)- Modern selectors â
:has()as parent selector,:is()vs:where()specificity difference,:emptyfor conditional display