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.:disabledand:enabled– a field that can or can't be used.:requiredand:optional– fields with or without therequiredattribute.:validand:invalid– the value passes or fails the field's rules (liketype="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-childand: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 examplep: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,:activeand:focus-visibleto give interactive elements clear feedback. - Never remove focus outlines without giving a visible replacement.
- Form pseudo-classes like
:checked,:disabledand:user-invalidstyle forms without JavaScript. :nth-child(),:first-child,:last-childand:not()select elements by position.