By Rahul — Google Frontend Engineer
The Difference
- Pseudo-classes (single colon
:) select elements in a specific STATE::hover,:focus,:first-child - Pseudo-elements (double colon
::) create VIRTUAL elements:::before,::after,::placeholder
Common Pseudo-Classes
State-based
a:hover { color: blue; } /* Mouse over */
a:active { color: red; } /* Being clicked */
input:focus { outline: 2px solid blue; } /* Has focus */
input:focus-visible { outline: 2px solid blue; } /* Keyboard focus only */
input:disabled { opacity: 0.5; } /* Disabled state */
input:checked { accent-color: green; } /* Checked checkbox/radio */Structural
li:first-child { font-weight: bold; }
li:last-child { border-bottom: none; }
li:nth-child(odd) { background: #f5f5f5; } /* Zebra striping */
li:nth-child(3n) { color: red; } /* Every 3rd item */
p:first-of-type { font-size: 1.2em; } /* First p, ignoring other elements */
div:empty { display: none; } /* No children */Negation and Selection
/* All links except the active class */
a:not(.active) { color: gray; }
/* Elements that match any of the selectors */
:is(h1, h2, h3) { font-family: serif; }
/* Like :is but with zero specificity */
:where(h1, h2, h3) { margin-bottom: 1rem; }
/* Has a descendant that matches (game-changer!) */
div:has(> img) { padding: 0; } /* div that directly contains an img */
form:has(:invalid) { border: 2px solid red; } /* form with invalid inputs */Common Pseudo-Elements
/* Add content before/after an element */
.required::before {
content: "* ";
color: red;
}
blockquote::before {
content: open-quote;
font-size: 2em;
}
/* Style the placeholder text */
input::placeholder {
color: #999;
font-style: italic;
}
/* Style text selection */
::selection {
background: #ffcc00;
color: black;
}
/* Style the first line or letter */
p::first-line { font-weight: bold; }
p::first-letter { font-size: 2em; float: left; }Real-World Patterns
Custom Checkbox
/* Hide native checkbox, style with pseudo-elements */
input[type="checkbox"] {
appearance: none;
width: 20px;
height: 20px;
border: 2px solid #ccc;
border-radius: 4px;
}
input[type="checkbox"]:checked::after {
content: "✓";
display: block;
text-align: center;
color: white;
}
input[type="checkbox"]:checked {
background: blue;
border-color: blue;
}Tooltip with ::after
[data-tooltip]:hover::after {
content: attr(data-tooltip);
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
padding: 4px 8px;
background: black;
color: white;
border-radius: 4px;
font-size: 12px;
white-space: nowrap;
}Production Tips
::beforeand::afterrequire thecontentproperty (even if empty:content: ''):focus-visibleis better than:focusfor keyboard accessibility — it does not show outlines on mouse clicks:has()is the "parent selector" CSS never had — use it for conditional parent styling- Pseudo-elements cannot be added to void elements (img, input, br)
Summary
Pseudo-classes select states (:hover, :nth-child). Pseudo-elements create virtual elements (::before, ::after). The :has() pseudo-class is a game-changer for parent-based styling. Master these and you can build complex UI without extra HTML or JavaScript.