CSS Pseudo-Classes
A pseudo-class selects an element based on its state or position — things that are not written in the HTML itself. You cannot look at a link and tell whether a user is hovering it; that state changes moment to moment. Pseudo-classes let CSS respond to those live conditions.
All pseudo-classes use a colon prefix: :hover, :focus, :first-child.
User Interaction Pseudo-Classes
These respond to what the user does with a mouse, keyboard, or touch.
:hover
Applies when the user's mouse pointer is over an element.
button:hover {
background-color: darkblue;
color: white;
}
DIAGRAM — :hover state:
Normal state: [ Click Me ] ← gray background
On hover: [ Click Me ] ← blue background (mouse is over it)
After hover: [ Click Me ] ← gray again
:focus
Applies when an element receives keyboard focus — usually after tabbing to it or clicking an input field.
input:focus {
border: 2px solid blue;
outline: none;
}
Important: Never remove :focus styles without providing an alternative. Keyboard users need visible focus indicators to know where they are on the page.
:active
Applies at the moment a user clicks or taps an element — just the instant of the press, not before or after.
button:active {
transform: scale(0.98);
background-color: darkred;
}
:visited and :link
These two work on anchor tags.
:link— a link the user has not visited yet:visited— a link the user has visited before
a:link { color: blue; }
a:visited { color: purple; }
Form State Pseudo-Classes
These target form elements based on their current state.
:checked
Applies to checkboxes and radio buttons that are currently checked.
input:checked + label {
font-weight: bold;
color: green;
}
:disabled and :enabled
input:disabled {
background-color: #eee;
cursor: not-allowed;
}
input:enabled {
background-color: white;
}
:required and :optional
input:required {
border-left: 4px solid red;
}
input:optional {
border-left: 4px solid gray;
}
:valid and :invalid
These apply based on whether the input value passes its built-in validation (like type="email" or required).
input:valid { border-color: green; }
input:invalid { border-color: red; }
Structural Pseudo-Classes
These select elements based on their position inside a parent.
:first-child and :last-child
DIAGRAM — first-child and last-child:
<ul>
<li>Item 1</li> ← :first-child
<li>Item 2</li>
<li>Item 3</li> ← :last-child
</ul>
li:first-child { font-weight: bold; }
li:last-child { color: gray; }
:nth-child()
Selects elements by their position number inside a parent. You can use a number, a keyword, or a formula.
/* Every second row */
tr:nth-child(even) {
background-color: #f2f2f2;
}
/* Every odd row */
tr:nth-child(odd) {
background-color: white;
}
/* Every 3rd element */
li:nth-child(3n) {
color: red;
}
/* 3rd element exactly */
p:nth-child(3) {
font-size: 1.2rem;
}
DIAGRAM — nth-child(2n) on a list:
Item 1 ← 1 → not selected
Item 2 ← 2 → ✅ selected (2n: n=1)
Item 3 ← 3 → not selected
Item 4 ← 4 → ✅ selected (2n: n=2)
Item 5 ← 5 → not selected
Item 6 ← 6 → ✅ selected (2n: n=3)
:nth-last-child()
Same as :nth-child() but counts from the end.
li:nth-last-child(1) { /* same as :last-child */ }
li:nth-last-child(2) { /* second from the end */ }
:only-child
Selects an element that is the only child of its parent.
p:only-child {
font-size: 1.3rem;
}
:first-of-type and :last-of-type
These work like :first-child but only count elements of the same type.
DIAGRAM — first-child vs first-of-type:
<div>
<h2>Heading</h2> ← :first-child AND h2:first-of-type
<p>Para 1</p> ← p:first-of-type (first p in parent)
<p>Para 2</p>
</div>
p:first-child → nothing! The first child is an h2, not a p.
p:first-of-type → selects "Para 1" ✅
:nth-of-type()
/* Style every 2nd image */
img:nth-of-type(2n) {
float: right;
}
:not() — The Negation Pseudo-Class
:not() selects everything that does not match the selector inside it.
/* All buttons except the one with class .cancel */
button:not(.cancel) {
background-color: blue;
color: white;
}
/* All inputs except submit buttons */
input:not([type="submit"]) {
border: 1px solid gray;
}
:is() and :where() — Grouping Made Easy
:is() lets you write one rule that matches multiple selectors at once. It reduces repetition.
/* Without :is() — repetitive */
h1, h2, h3, h4 {
font-family: Georgia, serif;
}
/* With :is() — cleaner */
:is(h1, h2, h3, h4) {
font-family: Georgia, serif;
}
:where() works exactly like :is() but has zero specificity. This makes it easy to override with any other rule.
:has() — The Parent Selector
:has() selects a parent based on what it contains. This was impossible in CSS for a long time and is now supported in all modern browsers.
/* Style a card that contains an image */
.card:has(img) {
border: 2px solid blue;
}
/* Style a section that has no h2 */
section:not(:has(h2)) {
background-color: lightyellow;
}
:root
:root targets the root element of the page, which is the <html> element. It is commonly used to declare CSS custom properties (variables).
:root {
--primary-color: #3498db;
--font-size-base: 16px;
}
Common Pseudo-Class Quick Reference
| Pseudo-Class | What It Targets |
|---|---|
| :hover | Element under the mouse |
| :focus | Element with keyboard focus |
| :active | Element being clicked |
| :checked | Checked checkbox or radio |
| :disabled | Disabled form field |
| :first-child | First child element |
| :last-child | Last child element |
| :nth-child(n) | Element at position n |
| :not(x) | Anything that is not x |
| :is(a,b,c) | Any element that matches a, b, or c |
| :has(x) | Element that contains x |
| :root | The html element |
Pseudo-classes let you style interactive states and structural positions without adding extra classes to your HTML. They keep your markup clean while giving you precise control over how elements look at every moment.
