Topic 1.4
Pseudo-classes
In one line
Pseudo-classes start with one colon and match elements by their state (hovered, focused, checked, disabled, visited) or by their position among siblings (first, last, every second, empty).
Think of it like this
A traffic light. It is the same physical light all day, but it looks different depending on its state: red, amber or green. A pseudo-class lets CSS style one element differently depending on its state right now ("while the mouse is over it", "while it's ticked") or its place in a line ("the first car", "every second car").
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Pseudo-class
- A keyword after one colon, like
:hover, that matches elements in a certain state or position. - State
- The condition an element is in right now: hovered, focused, checked, disabled…
- Focus
- Being the element that receives what you type or which key you press. Tab moves focus.
- Focus ring
- The outline the browser draws around the focused element so keyboard users can see it.
- Structural pseudo-class
- One that matches by position among siblings, like
:first-childor:nth-child(2). - Formula An+B
- A pattern in
:nth-child(). The browser tries n = 0, 1, 2… and matches each resulting position. - Of-type
- Counting only siblings with the same tag name, ignoring other elements.
- Pointer
- A mouse, trackpad, pen or finger: anything that points at the screen.
Step by step
01Respond to the mouse and the keyboard
Add a hover colour, a pressed look, and a clear focus ring for keyboard users. :focus-visible shows the ring when it helps (Tab key) without showing it after every mouse click.
button:hover {
background: #dbeafe;
}
button:active {
transform: translateY(1px);
}
button:focus-visible {
outline: 3px solid #2563eb;
outline-offset: 2px;
}02Style form states
:checked and :disabled come from the control's real state, so CSS stays in sync with what the user did, with no JavaScript.
input:checked + label {
font-weight: 700;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}03Pick items by position
:first-child and :last-child are great for removing a border or margin at the edges of a list. :nth-child(even) gives zebra stripes.
li:first-child { font-weight: 700; }
li:last-child { border-bottom: none; }
li:nth-child(even) { background: #f3f4f6; }04Work out a formula
For :nth-child(3n+1), substitute n = 0, 1, 2, 3: you get 1, 4, 7, 10. Those positions match. Negative results and 0 are ignored, because positions start at 1.
For :nth-child(-n+3): n = 0, 1, 2 give 3, 2, 1, and n = 3 gives 0 (ignored). So it matches the first three.
05Count only one tag with of-type
In <h3>…</h3><p>A</p><p>B</p>, p:first-child matches nothing, because the first child is the h3. p:first-of-type matches A: it only counts the p siblings.
article p:first-of-type {
font-size: 1.2em;
}06Hide containers with nothing in them
:empty matches an element with no children and no text. Write the HTML with nothing between the tags, not even a space.
.alert:empty {
display: none;
}Try it yourself
- 1
See focus-visible in action
In the first example, click the Press me button with the mouse, then click in empty space and press Tab until it is focused. Which time do you see the orange ring? Only with Tab.
- 2
Predict a formula
In the second example, add
console.log("2n+3", texts(".list li:nth-child(2n+3)"));. Work it out first: n = 0, 1, 2, 3 gives 3, 5, 7, 9 (no ninth item). Expect[ '3', '5', '7' ]. - 3
Fix the first-child surprise
In the second example, change
.mixed p:first-of-typeto.mixed p:first-child. Predict: does any paragraph turn green? (No.) Move the<h3>below the first<p>and run again.
Code & diagrams
Click the button with the mouse: no orange ring. Press Tab to reach it: the ring appears. That difference is `:focus-visible`. Untick the box and the label loses its bold.
Preview
Console
Nothing logged yet.Expected console output
checked: 1
disabled: 1
enabled buttons: 1`p:first-child` is empty because the first child of `.mixed` is the `h3`. Two slots are `:empty`: the truly empty one and the one with only a comment. The one with a space is not empty.
Preview
Console
Nothing logged yet.Expected console output
odd [ '1', '3', '5', '7' ]
3n [ '3', '6' ]
3n+1 [ '1', '4', '7' ]
-n+3 [ '1', '2', '3' ]
n+6 [ '6', '7', '8' ]
p:first-child []
p:first-of-type [ 'P one' ]
p:nth-of-type(2) [ 'P two' ]
p:last-child [ 'P three' ]
empty slots: 2Hover a row to highlight it. Change `even` to `odd`, or to `3n`, and predict which rows are striped before you run.
Preview
Console
Nothing logged yet.Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Remove focus outlines
Add *:focus { outline: none; } to the first example.
Break #2
`:last-child` matches nothing because something else is last
In a page body, write p:last-child { color: red; } where the last paragraph is followed by a <script> tag.
Break #3
Link states in the wrong order
In the first example, move the a:hover rule above a:visited.
Myth vs fact
Myth
:nth-child() counts from 0 like arrays in JavaScript.
Fact
It counts from 1. :nth-child(1) is the first child, the same as :first-child.
Myth
p:first-child means the first paragraph.
Fact
It means a paragraph that is also its parent's first child. For the first paragraph whatever comes before it, use p:first-of-type.
Myth
:hover works the same on phones.
Fact
Touch screens have no hover. A tap may trigger :hover and leave it stuck until you tap elsewhere. Never hide essential content behind hover; Phase 9 shows @media (hover: hover) for hover-only styles.
Myth
:focus and :focus-visible are the same.
Fact
:focus matches every focused element. :focus-visible only matches when a visible indicator is useful: keyboard focus and text inputs, but usually not a mouse click on a button.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
:nth-child(An+B of S)filters by a selector before counting:li:nth-child(odd of .visible)stripes only the visible items, so hiding an item doesn't break the zebra pattern. This form is Baseline 2023. - ▸
:visitedis deliberately limited: only colour properties apply, the colour's alpha comes from the unvisited style, andgetComputedStylereports the unvisited value. This stops a page from detecting your browsing history through styles. - ▸
Structural pseudo-classes are re-evaluated when siblings are added or removed. Inserting one item at the top of a long list with
:nth-childrules forces the browser to restyle every following sibling; a static class may be cheaper for very large, frequently changing lists. - ▸
Selectors Level 4 says
:emptyshould also match whitespace-only elements, but no browser has shipped that change because it would break existing sites. Treat whitespace as content.
Remember this
- 1
A pseudo-class is a keyword after a single colon, added to a selector:
a:hover,li:first-child. "Pseudo" means it acts like a class you never had to write: the browser adds and removes it automatically as things change. Each pseudo-class adds the same specificity as a class. - 2
User-action states.
:hovermatches while a pointer (mouse) is over the element.:activematches while it is being pressed (mouse button down, or a tap).:focusmatches the element that currently receives keyboard input (after a click on a field or pressing Tab).:focus-visiblematches only when the browser decides a visible focus ring should show: always for keyboard navigation and text fields, but not after a mouse click on a button. Style focus with:focus-visibleand never remove the outline without a replacement: keyboard users need it to see where they are. - 3
Link and form states.
:linkmatches unvisited links and:visitedlinks the user has been to. For privacy, browsers only let:visitedchange colours (such ascolor,background-color,border-color,outline-color) and JavaScript always sees the unvisited style. Write link states in the order:link,:visited,:hover,:active("LoVe HAte") so later states win.:checkedmatches ticked checkboxes, selected radio buttons and selected<option>s.:disabledand:enabledmatch form controls by whether they can be used. - 4
Position among siblings (structural pseudo-classes).
:first-childand:last-childmatch an element that is the first or last child of its parent.:nth-child(n)counts position from 1 (not 0)::nth-child(3)is the third child. It acceptsodd,evenand formulasAn+B: the browser tries n = 0, 1, 2, 3… and matches every position the formula produces.2n+1= 1, 3, 5 (odd),3n= 3, 6, 9,-n+3= 3, 2, 1 (the first three),n+4= 4, 5, 6… (from the fourth on).:nth-last-child()counts from the end. - 5
Of-type versions.
:first-childcounts all siblings, whatever their tag:p:first-childonly matches apthat is the very first child. If a heading comes first, nopmatches.:first-of-type,:last-of-typeand:nth-of-type()count only siblings of the same tag:p:first-of-typeis the firstpamong its siblings even if a heading comes before it. - 6
:emptymatches an element with no children at all: no elements and no text. Comments don't count, but a single space or line break does, so<div> </div>is not empty. It's useful for hiding empty containers:.alert:empty { display: none; }.
Explain it without notes
What is the difference between :focus and :focus-visible, and why does it matter for accessibility?
Explain li:nth-child(-n+3) and li:nth-child(n+4). What does each match?
What is the difference between p:first-child and p:first-of-type? Give a case where only one matches.
Why can't you change the font size of visited links?
In what order should you write link pseudo-classes, and why?
Practice
Style a ul.menu so the first item has no top border, every item after it has a 1px grey top border, and the last item is bold, using pseudo-classes.
Make every third card in a .grid of .card elements have a gold border, starting with the third.
Give a checkbox's label (written right after the checkbox) a line-through when the box is ticked.
Trade-offs
- ↔
Structural pseudo-classes keep HTML clean (no
.oddor.lastclasses), but they depend on the exact sibling list; any extra element (a script, an ad slot) shifts the count. - ↔
Hover effects add polish on desktop but don't exist on touch; anything important must also work by tap, focus and keyboard.
Done when you can
I can style hover, active, focus and focus-visible states.
I can use
:checked,:disabledand:enabledon form controls.I can work out what an
An+Bformula matches.I know when to use
:first-of-typeinstead of:first-child.I know what counts as
:empty.I never remove a focus outline without a visible replacement.