User-Action Pseudo-classes
These fire in response to user interaction — the most commonly used pseudo-class group.
/* :hover — pointer is over the element */
.btn:hover {
background: #0f5fa0;
transform: translateY(-2px);
}
/* :focus — element has keyboard/programmatic focus */
input:focus,
button:focus {
outline: 2px solid #1572B6;
outline-offset: 3px;
}
/* :active — element is being activated (mousedown / keydown) */
.btn:active {
transform: translateY(0);
box-shadow: none;
}
/* :focus-visible — focus only when navigating by keyboard (not mouse click) */
button:focus-visible {
outline: 2px solid #1572B6;
}
/* :focus-within — parent has a focused descendant */
.form-group:focus-within label {
color: #1572B6;
font-weight: 600;
}
Link Pseudo-classes
Always declare in LVHA order to avoid specificity conflicts: :link, :visited, :hover, :active.
/* :link — unvisited anchor */
a:link { color: #1572B6; }
/* :visited — already visited */
a:visited { color: #7b1fa2; }
/* :hover — pointer over link */
a:hover { text-decoration: underline; }
/* :active — being clicked */
a:active { color: #e44d26; }
Structural Pseudo-classes
Target elements based on their position in the document tree — no class attribute needed.
/* :first-child / :last-child */
li:first-child { border-top: none; }
li:last-child { border-bottom: none; }
/* :nth-child(n) — n is a number, keyword, or An+B formula */
tr:nth-child(even) { background: #f5f5f5; } /* zebra rows */
tr:nth-child(odd) { background: #ffffff; }
li:nth-child(3) { font-weight: bold; } /* 3rd item only */
li:nth-child(2n+1) { color: #1572B6; } /* odd items */
/* :nth-last-child(n) — count from end */
li:nth-last-child(1) { /* last item — same as :last-child */ }
li:nth-last-child(2) { /* second to last */ }
/* :only-child — element is the only child of its parent */
.icon:only-child { margin: 0 auto; }
/* :first-of-type / :last-of-type / :nth-of-type */
p:first-of-type { font-size: 1.1em; } /* first in its parent */
h2:nth-of-type(2) { margin-top: 2rem; } /* second h2 */
/* :root — the root element () */
:root { font-size: 16px; }
/* :empty — element with no children (including no text) */
td:empty { background: #fafafa; }
:not() – Negation
:not() matches elements that do not match the argument selector. Modern CSS allows complex selectors and comma-separated lists inside :not().
/* All except those with class .note */
p:not(.note) { color: #333; }
/* All buttons except disabled ones */
button:not(:disabled) { cursor: pointer; }
/* All li except first and last */
li:not(:first-child):not(:last-child) {
border-top: 1px solid #eee;
}
/* Multiple selectors in :not() (CSS Selectors Level 4) */
input:not([type="submit"], [type="reset"]) {
border: 1px solid #ccc;
}
/* Remove margin from last element in a container */
.container > *:not(:last-child) { margin-bottom: 1rem; }
Form State Pseudo-classes
Style form controls based on their validation state, checked state, or disabled state — without JavaScript.
/* :checked — checkbox or radio is selected */
input[type="checkbox"]:checked + label {
font-weight: bold;
color: #1572B6;
}
/* Custom toggle using :checked */
.toggle-input:checked ~ .toggle-knob {
transform: translateX(20px);
background: #1572B6;
}
/* :disabled / :enabled */
input:disabled {
opacity: 0.5;
cursor: not-allowed;
background: #f0f0f0;
}
button:enabled:hover { background: #0f5fa0; }
/* :required / :optional */
input:required { border-left: 3px solid #e44d26; }
input:optional { border-left: 3px solid #ccc; }
/* :valid / :invalid — only after user interacts (use :user-valid for that) */
input:valid { border-color: #2e7d32; }
input:invalid { border-color: #e44d26; }
/* :placeholder-shown — input currently showing its placeholder */
input:placeholder-shown { font-style: italic; }
/* :read-only / :read-write */
input:read-only { background: #f5f5f5; color: #777; }
:is(), :where(), :has()
Modern pseudo-classes that reduce repetition and enable parent selection.
/* :is() — match any of several selectors, takes highest specificity of args */
:is(h1, h2, h3, h4) { line-height: 1.3; }
:is(article, section, aside) p { margin-bottom: 1em; }
/* :where() — same as :is() but always zero specificity */
:where(h1, h2, h3) { font-weight: 700; } /* easy to override */
/* :has() — "parent selector" — select element IF it has matching descendant */
/* Card that has an image — give it no padding */
.card:has(img) { padding: 0; }
/* Label next to a required input */
.form-group:has(input:required) label::after {
content: ' *';
color: #e44d26;
}
/* Highlight a li containing a checked checkbox */
li:has(input:checked) {
background: #e3f2fd;
text-decoration: line-through;
}
📋 Summary
- :hover / :focus / :active — user-action states. Use
:focus-visiblefor keyboard-only focus rings. - :focus-within — parent gets a style when any descendant is focused.
- Link order LVHA:
:link→:visited→:hover→:active. - :nth-child(An+B) — powerful structural targeting: even, odd, every 3rd, etc.
- :not() — negate any selector. Combine multiples:
:not(:first-child):not(:last-child). - :checked / :disabled / :valid / :invalid — form states without JavaScript.
- :is() — group selectors, keeps high specificity. :where() — same but zero specificity (easy override).
- :has() — parent selector. Select an ancestor based on its descendants.
Frequently Asked Questions
:hover activates when the pointer device is positioned over the element. :focus activates when the element receives focus — by keyboard tab, mouse click on a focusable element, or programmatic element.focus(). For accessibility, always style both — keyboard users never trigger :hover, and mouse users sometimes don't trigger :focus visually (browser default behavior varies). Use :focus-visible to show focus rings only for keyboard navigation.
:nth-child(n) counts all sibling children regardless of element type, then checks if the element matches the selector. :nth-of-type(n) counts only siblings of the same element type. Example: if a <div> contains <h2>, <p>, <p>, <p> — then p:nth-child(2) matches the first <p> (it's the 2nd child overall), while p:nth-of-type(2) matches the second <p> (it's the 2nd <p> sibling).
:has() reached baseline support in December 2023 with Firefox 121 adding support. As of 2026, browser support is above 90% globally (Chrome 105+, Safari 15.4+, Firefox 121+). For production use, it is safe to use as progressive enhancement — if the browser doesn't support :has(), the rule is simply ignored and layout falls back gracefully.