CSS pseudo-classes and pseudo-elements are some of the most underused CSS features — letting you style elements based on state, position, and relationships without JavaScript or extra HTML. :has() (the parent selector) finally arrived in modern browsers, and :is()/:where() slash specificity headaches.
⚡ TL;DR: Pseudo-class: :hover, :focus, :active, :nth-child, :not, :has, :is, :where. Pseudo-element: ::before, ::after, ::placeholder, ::first-line. :has() selects parents. :not() excludes. :nth-child(even/odd/3n+1) for patterns. ::before/::after add content without HTML.
State pseudo-classes
/* :hover :focus :active :visited */
button:hover { background: #1d4ed8; }
button:active { transform: scale(0.98); }
input:focus { outline: 2px solid #2563eb; outline-offset: 2px; }
a:visited { color: purple; }
/* :focus-visible — keyboard focus only (not mouse click) */
button:focus-visible { outline: 2px solid #2563eb; } /* Accessibility! */
button:focus:not(:focus-visible) { outline: none; } /* Remove for mouse */
/* :disabled :enabled :checked :required :optional */
input:disabled { opacity: 0.5; cursor: not-allowed; }
input:invalid { border-color: #ef4444; }
input:valid { border-color: #10b981; }
:nth-child and structural selectors
/* :first-child :last-child :only-child :nth-child */
li:first-child { font-weight: bold; }
li:last-child { border-bottom: none; }
li:nth-child(even) { background: #f8fafc; }
li:nth-child(3n+1) { /* Every 3rd, starting at 1: 1,4,7,10... */ }
/* :nth-of-type: count only matching type */
p:nth-of-type(2) { /* Second , ignoring other elements */ }
/* :not() — exclude specific elements */
button:not([disabled]) { cursor: pointer; }
li:not(:last-child) { border-bottom: 1px solid #e2e8f0; }
:has() — the parent selector
/* Select parent based on child — CSS's biggest missing feature, now here! */
/* Form label if required input inside */
label:has(input:required) { font-weight: bold; }
label:has(input:required)::after { content: ' *'; color: red; }
/* Card with image gets different layout */
.card:has(img) { grid-template-rows: auto 1fr; }
.card:not(:has(img)) { padding: 32px; }
/* Sibling selection with :has + ~ */
/* When checkbox is checked, style following sibling */
:has(input[type=checkbox]:checked) ~ .content { display: block; }
::before and ::after pseudo-elements
/* Add decorative content without extra HTML */
/* Tooltip on hover */
[data-tooltip]:hover::before {
content: attr(data-tooltip); /* Reads HTML attribute */
position: absolute;
background: #1e293b;
color: white;
padding: 4px 8px;
border-radius: 4px;
white-space: nowrap;
}
/* Counter badges */
.badge::after {
content: counter(badge-count);
background: #ef4444;
border-radius: 50%;
}
/* Decorative dividers */
.section-title::before, .section-title::after {
content: '';
flex: 1;
height: 1px;
background: #e2e8f0;
}
- ✅ :focus-visible for keyboard-only focus styles (accessibility)
- ✅ :has() for parent selection — no more JS for this pattern
- ✅ :is()/:where() to reduce specificity in complex selectors
- ✅ ::before/::after for decorative content without HTML
- ❌ :has() not supported in Firefox before 121 — check caniuse
- ❌ Excessive use of ::before/after for functional content — use real HTML
External reference: MDN CSS Pseudo-classes.
Recommended Reading
→ Designing Data-Intensive Applications — The bible of distributed systems and production engineering at scale.
→ The Pragmatic Programmer — Timeless engineering wisdom every senior developer needs.
Affiliate links. We earn a small commission at no extra cost to you.
Free Weekly Newsletter
🚀 Join 2,000+ Senior Developers
Get expert-level JavaScript, Python, AWS, system design and AI secrets every week. Zero fluff, pure signal.
Discover more from CheatCoders
Subscribe to get the latest posts sent to your email.
