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-class | When it applies |
|---|---|
:hover | The mouse pointer is over the element |
:active | The element is being pressed |
:focus | The element has focus from a click or the keyboard |
:focus-visible | The element has focus from the keyboard (the best place for focus rings) |
:visited | A 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-class | Selects |
|---|---|
:first-child / :last-child | The 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-child | An element with no siblings |
:first-of-type | The 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) aselects 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,:activeand:focus-visiblerespond to the visitor's actions.:first-child,:last-childand:nth-child()select by position.:checked,:invalidand:user-invalidstyle 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.
Interactive button states
Hover, click and press Tab to focus the button in the result.
<button class="btn">Hover, press or tab to me</button>.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;
}Line-by-line explanation
- Lines 10 to 13On hover, lighten and add a hard shadow.
- Lines 15 to 18While pressed, the shadow disappears and the button "sinks".
- Lines 20 to 23A clear ring only for keyboard focus.
Zebra stripes and dividers
<ul class="list">
<li>Apples</li>
<li>Bananas</li>
<li>Cherries</li>
<li>Dates</li>
<li>Elderberries</li>
</ul>.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; }Line-by-line explanation
- Line 4Every odd row gets a tint.
- Line 5A divider under every item except the last.
- Line 6The first item is bold.
Form validation and :has()
Type an invalid email, then a valid one. Tick the checkbox to see :has() style the whole card.
<label>Email <input type="email" placeholder="you@site.com" required></label>
<label class="option"><input type="checkbox"> I agree to the terms</label>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;
}Line-by-line explanation
- Lines 1 to 4Red only after the user has typed something invalid.
- Lines 6 to 8Green when the value is valid.
- Lines 18 to 21The label changes style when it contains a checked checkbox.
Common CSS Pseudo-classes Mistakes
Using two colons
a::hover { color: red; }a:hover { color: red; }Pseudo-classes use one colon. Two colons are for pseudo-elements like ::before.
Hover-only interactions
.menu { display: none; }
.nav:hover .menu { display: block; }.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.
Change link colour on hover and stripe even rows.
a { color: tomato; }
tr:nth-child() { background: #f5f5f5; }
:hover for the mouse state, :nth-child(even) for every second row.Which selector targets every list item except the last?
:not() excludes whatever is inside it.Why prefer :focus-visible over :focus for outlines?
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>
The button should turn dark on hover and the first table row should be bold. The selectors are wrong. Fix them.
<button class="go">Go</button>
<table><tr><td>Header-ish</td></tr><tr><td>Row</td></tr></table>.go { background: #ffd23f; border: 0; padding: 10px 16px; }
.go::hover {
background: #15161b;
color: white;
}
tr:first {
font-weight: bold;
}- 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.
Which pseudo-class applies while the mouse is over an element?
- :active
- :over
- :mouse
- :hover
What does
.card:has(img)select?- Cards that contain an image
- Images inside cards
- Cards without images
- The first image in each card
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:
.plan:hovergets aborder-colorof#4467ff.plan:has(input:checked)gets abackgroundof#e1e6ff- Every
.planexcept the last hasmargin-bottom: 10px
<label class="plan"><input type="radio" name="p" checked> Monthly: £9</label>
<label class="plan"><input type="radio" name="p"> Yearly: £90</label>
<label class="plan"><input type="radio" name="p"> Lifetime: £200</label>.plan { display: block; padding: 12px; border: 2px solid #ccc; border-radius: 12px; font-family: sans-serif; }
- Checked plan is highlighted
- Unchecked plans aren't highlighted
- Spacing on all but the last
- A hover rule exists