Ad – 728×90
🔧 Advanced CSS

CSS Pseudo-classes – Select Elements by State

A pseudo-class is a keyword added to a selector that targets an element in a specific state or structural position — without adding extra HTML classes. They let you style links on hover, highlight focused form fields, zebra-stripe table rows, and disable styles on certain children, all purely in CSS. This lesson covers every practical pseudo-class group: user-action states, link states, structural selectors, form states, and negation.

⏱️ 20 min read 🎯 Intermediate 📅 Updated 2026 👁️ Lesson 2 of 5

User-Action Pseudo-classes

These fire in response to user interaction — the most commonly used pseudo-class group.

CSS – User-action pseudo-classes
/* :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;
}

Always declare in LVHA order to avoid specificity conflicts: :link, :visited, :hover, :active.

CSS – Link states
/* :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.

CSS – Structural pseudo-classes
/* :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; }

Ad – 336×280

:not() – Negation

:not() matches elements that do not match the argument selector. Modern CSS allows complex selectors and comma-separated lists inside :not().

CSS – :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.

CSS – Form state pseudo-classes
/* :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.

CSS – :is(), :where(), :has()
/* :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-visible for 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

What is the difference between :hover and :focus? +

: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.

What is the difference between :nth-child and :nth-of-type? +

: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).

Is :has() supported in all browsers? +

: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.