Difficulty: Intermediate
What is the difference between pseudo-classes and pseudo-elements?
Pseudo-classes select elements in a specific STATE (single colon :). Pseudo-elements create virtual ELEMENTS (double colon ::).
Pseudo-classes: - User action: :hover, :focus, :active, :focus-visible - Structural: :first-child, :last-child, :nth-child(), :nth-of-type() - State: :checked, :disabled, :valid, :invalid, :empty - Negation: :not(), :is(), :where(), :has()
Pseudo-elements: - ::before, ::after - insert content before/after - ::first-line, ::first-letter - style text parts - ::placeholder - style input placeholder - ::selection - style highlighted text
/* User action states */
.link:hover { color: blue; }
.link:active { color: red; }
.input:focus { border-color: blue; }
.input:focus-visible {
outline: 2px solid blue;
/* Only shows for keyboard focus, not mouse */
}
/* Structural */
li:first-child { font-weight: bold; }
li:last-child { border-bottom: none; }
li:nth-child(odd) { background: #f9f9f9; }
li:nth-child(3n) { color: red; } /* every 3rd */
li:nth-child(n+4) { opacity: 0.7; } /* 4th onwards */
/* Form states */
input:required { border-left: 3px solid red; }
input:valid { border-color: green; }
input:invalid { border-color: red; }
input:disabled { opacity: 0.5; }
checkbox:checked + label { font-weight: bold; }
/* Empty state */
.list:empty::after {
content: 'No items found';
}
:focus-visible is better than :focus for accessibility - it shows focus rings for keyboard users but not mouse users.
/* ::before and ::after (must have content) */
.quote::before {
content: '"';
font-size: 2rem;
color: #3b82f6;
}
.required-field::after {
content: ' *';
color: red;
}
/* Decorative elements without extra HTML */
.divider::after {
content: '';
display: block;
width: 50px;
height: 3px;
background: #3b82f6;
margin-top: 0.5rem;
}
/* Custom checkbox */
.checkbox::before {
content: '';
width: 1.25rem;
height: 1.25rem;
border: 2px solid #ccc;
border-radius: 0.25rem;
display: inline-block;
}
.checkbox:checked::before {
background: #3b82f6;
border-color: #3b82f6;
}
/* Text styling */
p::first-letter {
font-size: 2rem;
font-weight: bold;
float: left;
margin-right: 0.25rem;
}
/* Selection color */
::selection {
background: #3b82f6;
color: white;
}
::before and ::after require the content property (even if empty string). They're inline by default - set display: block or inline-block for sizing.
/* :is() - matches any selector in the list */
:is(h1, h2, h3) {
font-family: serif;
}
/* Same as: h1, h2, h3 { font-family: serif; } */
/* But :is() takes highest specificity in list */
/* :where() - same as :is() but ZERO specificity */
:where(h1, h2, h3) {
font-family: serif;
/* Easy to override */
}
/* :has() - parent selector (game changer!) */
.card:has(img) {
/* Style card only if it contains an image */
grid-template-rows: 200px 1fr;
}
.form-group:has(input:invalid) {
/* Style parent when child input is invalid */
border-color: red;
}
/* :has() for conditional layout */
.grid:has(> :nth-child(4)) {
/* If grid has 4+ children, use 2 columns */
grid-template-columns: 1fr 1fr;
}
:has() is the parent selector CSS never had. It styles a parent based on its children. :is() reduces repetition, :where() keeps zero specificity for easy overrides.
Pseudo-classes, Pseudo-elements, ::before, ::after, :nth-child, :hover, :focus