Complete CSS Course Module 6: Intermediate: The Cascade and Advanced Selectors

CSS Pseudo-classes: :hover, :focus, :nth-child and More

  • Beginner
  • Lesson 6.2
  • 5 min read
  • 25 of 77 in the course

In short

CSS pseudo-classes let you style an element based on its state or position, like :hover when the mouse is over a button or :nth-child for every second row of a table. What is a pseudo-class? A pseudo-class is a keyword you add to a selector with a single colon.

CSS pseudo-classes let you style an element based on its state or position, like :hover when the mouse is over a button or :nth-child for every second row of a table.

What is a pseudo-class?

A pseudo-class is a keyword you add to a selector with a single colon. It selects an element only when something is true about it. "Pseudo" means "not real": there is no class in your HTML, but the browser acts as if there were one when the condition is met.

a:hover {
  color: #dc2626;
}

This reads as: "select every link, but only while the mouse is over it". Each pseudo-class adds 0-1-0 to specificity, the same as a class.

User action states: :hover, :active and :focus

These pseudo-classes react to what the user is doing:

  • :hover – the mouse pointer is over the element.
  • :active – the element is being pressed (mouse button held down, or a finger tap).
  • :focus – the element has keyboard focus, for example an input you clicked into or a link you reached with the Tab key.
  • :focus-visible – the element has focus and the browser thinks a visible focus ring is helpful, mostly when the user is using a keyboard.
  • :focus-within – the element itself, or anything inside it, has focus.

Here is a friendly button with all its states:

<button class="btn">Join the course</button>
.btn {
  background: #2563eb;
  color: white;
  border: none;
  padding: 12px 24px;
  border-radius: 8px;
  font-size: 1rem;
  cursor: pointer;
}

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

.btn:active {
  background: #1e40af;
}

.btn:focus-visible {
  outline: 3px solid #f59e0b;
  outline-offset: 3px;
}

When you move the mouse over the button, it gets a little darker. When you press it, it gets darker still. When you reach it with the Tab key, a clear orange ring appears around it. Mouse users clicking the button will usually not see the ring, because :focus-visible only shows it when it helps.

Common mistake: Writing :focus { outline: none; } to hide the "ugly" focus ring. Keyboard users then cannot see where they are on the page. If you don't like the default ring, replace it with your own style using :focus-visible, never just remove it.

Tip: Write link states in the order :link, :visited, :hover, :active (remember "LoVe HAte"). They have the same specificity, so a later rule wins, and this order makes each state work as expected.

Form states: :checked, :disabled and :invalid

Forms have their own states, and pseudo-classes let you style them without any JavaScript:

  • :checked – a checkbox or radio button that is ticked.
  • :disabled and :enabled – a field that can or can't be used.
  • :required and :optional – fields with or without the required attribute.
  • :valid and :invalid – the value passes or fails the field's rules (like type="email").
  • :user-invalid – like :invalid, but only after the user has interacted with the field.
  • :placeholder-shown – the field is empty and showing its placeholder text.
<form class="signup">
  <label>Email <input type="email" required placeholder="you@example.com"></label>
  <label><input type="checkbox"> Send me the newsletter</label>
  <button type="submit" disabled>Sign up</button>
</form>
.signup input:user-invalid {
  border: 2px solid #dc2626;
  background: #fef2f2;
}

.signup input:valid {
  border: 2px solid #16a34a;
}

.signup input[type="checkbox"]:checked {
  accent-color: #16a34a;
}

.signup button:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

If you type "hello" in the email field and click away, it turns red, because "hello" is not a valid email. Type a real address and it turns green. The disabled button looks faded and shows a "not allowed" cursor. Using :user-invalid instead of :invalid is kinder: the field doesn't shout "error!" before the user has typed anything.

Structural pseudo-classes: :first-child, :nth-child and :not()

Structural pseudo-classes select elements by their position among their siblings (elements that share the same parent):

  • :first-child and :last-child – the first or last child of its parent.
  • :nth-child(2) – the second child.
  • :nth-child(odd) and :nth-child(even) – every odd or even child.
  • :nth-child(3n) – every third child (3, 6, 9...).
  • :nth-child(-n + 3) – the first three children.
  • :only-child – the element is the only child.
  • :not(selector) – everything that does not match the selector.

A striped price table is a classic use:

<table class="prices">
  <tr><td>Starter</td><td>$0</td></tr>
  <tr><td>Pro</td><td>$12</td></tr>
  <tr><td>Team</td><td>$40</td></tr>
  <tr><td>Business</td><td>$99</td></tr>
</table>
.prices {
  border-collapse: collapse;
  width: 100%;
}

.prices td {
  padding: 10px;
}

.prices tr:nth-child(odd) {
  background: #f3f4f6;
}

.prices tr:first-child {
  font-weight: bold;
}

Rows 1 and 3 get a light grey background, which makes long tables easier to read. The first row is bold.

Now :not(), which is perfect for menus with dividers between the items:

.menu li:not(:last-child) {
  border-bottom: 1px solid #e5e7eb;
}

Every menu item gets a bottom line, except the last one. Without :not() you would need two rules: one to add the border and one to remove it again.

Note: :nth-child() counts all siblings, whatever their tag. If you want to count only one type of element, use :nth-of-type(), for example p:nth-of-type(2) for the second paragraph.

Practice exercise

Build a list of five to-do items, each with a checkbox. Make every even item have a light background, remove the bottom border from the last item using :not(), and give each item a soft highlight when it is hovered. Finally, make the label text grey and crossed out when its checkbox is ticked.

Hint: for the last part, put the checkbox inside the <label> and use label:has(input:checked) with text-decoration: line-through. The :has() pseudo-class selects a parent that contains a matching element.

Key takeaways

  • Pseudo-classes start with one colon and select elements in a certain state or position.
  • Use :hover, :active and :focus-visible to give interactive elements clear feedback.
  • Never remove focus outlines without giving a visible replacement.
  • Form pseudo-classes like :checked, :disabled and :user-invalid style forms without JavaScript.
  • :nth-child(), :first-child, :last-child and :not() select elements by position.