Pseudo-classes

Difficulty: Intermediate

Pseudo-classes are special selectors that target elements in a specific state or position, without needing extra classes in the HTML. They are written with a single colon prefix (:hover, :focus, :nth-child). Pseudo-classes let you style elements based on user interaction (hovering, clicking, focusing), position within their parent (first child, nth child), form state (checked, disabled, valid), and more. They are fundamental for creating interactive and dynamic interfaces with pure CSS.

Interaction pseudo-classes respond to user actions. :hover activates when the user's mouse cursor is over an element. :focus activates when an element receives focus (via click or Tab key). :active activates while an element is being clicked (mouse button down). :focus-visible is the modern alternative to :focus that only shows focus styles when the user is navigating via keyboard (not on mouse click), which is better for user experience. For links, there is also :visited (already visited links) and :link (unvisited links). The recommended order for link styles is LVHA: :link, :visited, :hover, :active.

Structural pseudo-classes target elements based on their position within their parent. :first-child and :last-child select the first and last child element. :nth-child(n) is the most powerful - it accepts formulas like :nth-child(odd), :nth-child(even), :nth-child(3n) (every 3rd), or :nth-child(3n+1) (1st, 4th, 7th...). The related :nth-of-type(n) is similar but only counts elements of the same type, which avoids a common confusion when different element types are mixed.

The :not() pseudo-class is a negation selector that matches elements that do NOT match the given selector. For example, :not(.hidden) matches every element that does not have the 'hidden' class. In modern CSS (Selectors Level 4), :not() can accept complex selectors like :not(.card .title). The :not() pseudo-class is incredibly useful for 'everything except' patterns, like styling all links except the active one: a:not(.active) { opacity: 0.6; }.

Form-related pseudo-classes are essential for styling forms. :checked targets checked checkboxes and radio buttons. :disabled and :enabled target disabled/enabled form elements. :valid and :invalid respond to HTML5 form validation - an input with type="email" will match :invalid if the value is not a valid email. :placeholder-shown targets inputs that are currently showing placeholder text (meaning they are empty). These pseudo-classes enable sophisticated form styling without JavaScript.

Code examples

Interaction Pseudo-classes

/* Hover: mouse is over the element */
.btn {
  background-color: #2563eb;
  color: white;
  padding: 10px 20px;
  border: none;
  border-radius: 6px;
  cursor: pointer;
  transition: background-color 0.2s;
}

.btn:hover {
  background-color: #1d4ed8;
}

.btn:active {
  background-color: #1e40af;
  transform: scale(0.98);
}

/* Focus-visible: keyboard navigation only */
.btn:focus-visible {
  outline: 3px solid #93c5fd;
  outline-offset: 2px;
}

/* Link pseudo-classes (LVHA order) */
a:link { color: #2563eb; }
a:visited { color: #7c3aed; }
a:hover { color: #1d4ed8; text-decoration: underline; }
a:active { color: #1e40af; }

Interaction pseudo-classes respond to user actions. Use :focus-visible instead of :focus for better UX - it only shows focus rings on keyboard navigation, not on mouse clicks. Follow LVHA order for link styles.

Structural Pseudo-classes

/* First and last child */
li:first-child {
  font-weight: bold;
}

li:last-child {
  border-bottom: none;
}

/* nth-child: powerful positional targeting */
tr:nth-child(odd) {
  background-color: #f9fafb;  /* Zebra-striped table rows */
}

tr:nth-child(even) {
  background-color: white;
}

/* Every 3rd element */
.grid-item:nth-child(3n) {
  margin-right: 0;
}

/* First 3 elements */
li:nth-child(-n+3) {
  color: #059669;
  font-weight: 600;
}

/* Only child: element with no siblings */
.tab:only-child {
  border-radius: 8px;
}

/* nth-of-type: counts only matching elements */
p:nth-of-type(1) {
  font-size: 1.25rem; /* First <p>, ignoring other elements */
}

Structural pseudo-classes target elements by position. :nth-child(an+b) is the most versatile: odd/even for alternation, 3n for every 3rd, -n+3 for the first three. :nth-of-type counts only same-type siblings.

Negation and Form Pseudo-classes

/* :not() - select everything EXCEPT */
nav a:not(.active) {
  opacity: 0.7;
}

nav a:not(.active):hover {
  opacity: 1;
}

/* All children except the last */
.list li:not(:last-child) {
  border-bottom: 1px solid #e5e7eb;
}

/* Form pseudo-classes */
input:focus {
  border-color: #3b82f6;
  outline: none;
  box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}

input:disabled {
  background-color: #f3f4f6;
  cursor: not-allowed;
  opacity: 0.6;
}

input:valid {
  border-color: #10b981;
}

input:invalid {
  border-color: #ef4444;
}

input:placeholder-shown {
  border-color: #d1d5db; /* Show neutral border when empty */
}

The :not() pseudo-class excludes elements matching the inner selector. Form pseudo-classes (:valid, :invalid, :disabled, :placeholder-shown) enable styling based on form state without JavaScript.

Key points

Concepts covered

:hover, :focus, :active, :nth-child, :first-child, :last-child, :not, :focus-visible, State-based Selectors