CSS Pseudo-classes

Style elements by state and position: hover, focus, first, nth and more.

Lesson 22 of 49Intermediate13 min read

A pseudo-class selects an element when it's in a particular state or position: being hovered over, focused, checked, first in a list, or every other row in a table. Pseudo-classes start with a single colon, as in :hover.

They're how websites feel responsive without a single line of JavaScript. A button that changes colour under the mouse, a table with striped rows and a form field that turns red when the input is wrong are all pseudo-classes at work. They also keep your HTML clean, because you don't need extra classes like first or odd on each element.

Pseudo-classes for user actions

Pseudo-classWhen it applies
:hoverThe mouse pointer is over the element
:activeThe element is being pressed
:focusThe element has focus from a click or the keyboard
:focus-visibleThe element has focus from the keyboard (the best place for focus rings)
:visitedA link the visitor has already opened

Touch screens have no real hover, so never hide important information behind :hover alone. Make sure the same content is reachable by tapping or with the keyboard.

Pseudo-classes for position among siblings

Pseudo-classSelects
:first-child / :last-childThe first or last child of its parent
:nth-child(2)The second child
:nth-child(odd) / (even)Every other child, ideal for striped rows
:nth-child(3n)Every third child
:only-childAn element with no siblings
:first-of-typeThe first element of its type

Because these depend on position, they update automatically. Add or remove a table row and the stripes rearrange themselves.

Pseudo-classes for form states

:checked, :disabled, :required, :invalid, :valid and :placeholder-shown let you style forms based on what the visitor has done. :user-invalid is especially useful: it only applies after the visitor has interacted with a field, so an empty form doesn't greet people with red error borders.

Logic with :not(), :is() and :has()

  • li:not(:last-child) selects every list item except the last one, which is perfect for dividers between items.
  • :is(h1, h2, h3) a selects links inside any of those headings, without repeating the rest of the selector.
  • .card:has(img) selects a card that contains an image. :has() is often called the "parent selector", and it can now replace many small JavaScript tricks.

CSS Pseudo-classes Syntax

Pseudo-class syntax

a:hover { }
button:focus-visible { }
li:nth-child(odd) { }
input:invalid { }
.card:has(img) { }
li:not(:last-child) { }

CSS Pseudo-classes Summary

  • Pseudo-classes start with one colon and select elements by state or position.
  • :hover, :active and :focus-visible respond to the visitor's actions.
  • :first-child, :last-child and :nth-child() select by position.
  • :checked, :invalid and :user-invalid style form fields by state.
  • :not(), :is() and :has() add logic to selectors.

CSS Pseudo-classes Examples

Each example runs in the page. Edit the code and press Run to see what changes.

Example 1

Interactive button states

Hover, click and press Tab to focus the button in the result.

.btn {
  padding: 12px 20px;
  border: 2px solid #15161b;
  border-radius: 12px;
  background: #ffd23f;
  font: 700 16px sans-serif;
  cursor: pointer;
}

.btn:hover {
  background: #ffe066;
  box-shadow: 4px 4px 0 #15161b;
}

.btn:active {
  box-shadow: none;
  transform: translate(2px, 2px);
}

.btn:focus-visible {
  outline: 3px solid #4467ff;
  outline-offset: 3px;
}
ResultEdit the code, then press Run or Ctrl+Enter
Line-by-line explanation
  1. Lines 10 to 13On hover, lighten and add a hard shadow.
  2. Lines 15 to 18While pressed, the shadow disappears and the button "sinks".
  3. Lines 20 to 23A clear ring only for keyboard focus.
Example 2

Zebra stripes and dividers

.list { list-style: none; padding: 0; width: 220px; font-family: sans-serif; }
.list li { padding: 8px 12px; }

.list li:nth-child(odd) { background: #f3ede2; }
.list li:not(:last-child) { border-bottom: 1px solid #ddd; }
.list li:first-child { font-weight: bold; }
.list li:hover { background: #fff1c2; }
ResultEdit the code, then press Run or Ctrl+Enter
Line-by-line explanation
  1. Line 4Every odd row gets a tint.
  2. Line 5A divider under every item except the last.
  3. Line 6The first item is bold.
Example 3

Form validation and :has()

Type an invalid email, then a valid one. Tick the checkbox to see :has() style the whole card.

input:user-invalid {
  border: 2px solid #e5484d;
  background: #ffe4e4;
}

input:valid {
  border: 2px solid #17a865;
}

.option {
  display: block;
  margin-top: 12px;
  padding: 10px;
  border: 2px solid #ddd;
  border-radius: 10px;
}

.option:has(input:checked) {
  border-color: #17a865;
  background: #d6f4e4;
}
ResultEdit the code, then press Run or Ctrl+Enter
Line-by-line explanation
  1. Lines 1 to 4Red only after the user has typed something invalid.
  2. Lines 6 to 8Green when the value is valid.
  3. Lines 18 to 21The label changes style when it contains a checked checkbox.

Common CSS Pseudo-classes Mistakes

Using two colons

Don't
a::hover { color: red; }
Do
a:hover { color: red; }

Pseudo-classes use one colon. Two colons are for pseudo-elements like ::before.

Hover-only interactions

Don't
.menu { display: none; }
.nav:hover .menu { display: block; }
Do
.nav:hover .menu,
.nav:focus-within .menu { display: block; }

Phones have no hover, and keyboard users never "hover". Add :focus-within (and ideally a click with JavaScript).

CSS Pseudo-classes in Real Projects

Interfaces that respond

Every hover effect on a button, the highlighted row in a table, the red border on a wrong form field and the selected state of a radio card in a checkout come from pseudo-classes. Modern apps use :has() for things that used to need JavaScript, such as styling a form row when its input is invalid.

CSS Pseudo-classes Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Fill in the blankseasy

Change link colour on hover and stripe even rows.

a { color: tomato; }
tr:nth-child() { background: #f5f5f5; }
Multiple choicemedium

Which selector targets every list item except the last?

Multiple choicemedium

Why prefer :focus-visible over :focus for outlines?

Predict the outputmedium

Which items are bold?

<ul><li>A</li><li>B</li><li>C</li><li>D</li><li>E</li><li>F</li></ul>
<style> li:nth-child(3n) { font-weight: bold; } </style>
Fix the bugmedium

The button should turn dark on hover and the first table row should be bold. The selectors are wrong. Fix them.

.go { background: #ffd23f; border: 0; padding: 10px 16px; }

.go::hover {
  background: #15161b;
  color: white;
}

tr:first {
  font-weight: bold;
}
ResultEdit the code, then press Run or Ctrl+Enter
Checks
  • The hover rule is valid
  • The first row is bold
  • The second row is not bold

CSS Pseudo-classes Quiz

3 questions to check what you learned.

  1. Which pseudo-class applies while the mouse is over an element?

    • :active
    • :over
    • :mouse
    • :hover
  2. What does .card:has(img) select?

    • Cards that contain an image
    • Images inside cards
    • Cards without images
    • The first image in each card
  3. li:nth-child(odd) selects items…

    • 2, 4, 6…
    • 1, 3, 5, 7…
    • Only the first
    • Items with odd text

CSS Pseudo-classes Challenge

Style a plan picker

Using pseudo-classes only:

  1. .plan:hover gets a border-color of #4467ff
  2. .plan:has(input:checked) gets a background of #e1e6ff
  3. Every .plan except the last has margin-bottom: 10px
.plan { display: block; padding: 12px; border: 2px solid #ccc; border-radius: 12px; font-family: sans-serif; }
ResultEdit the code, then press Run or Ctrl+Enter
Checks
  • Checked plan is highlighted
  • Unchecked plans aren't highlighted
  • Spacing on all but the last
  • A hover rule exists