Command Palette

Search for a command to run...

Hectal
PHASE 1Beginner ~26 min· topic 4 of 6

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-child or :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.

style.csswhole filecss
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.

style.csswhole filecss
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.

style.csswhole filecss
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.

Work out a formuladiagram
Rendering diagram…

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.

style.csswhole filecss
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.

style.csswhole filecss
.alert:empty {
  display: none;
}

Try it yourself

  1. 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. 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. 3

    Fix the first-child surprise

    In the second example, change .mixed p:first-of-type to .mixed p:first-child. Predict: does any paragraph turn green? (No.) Move the <h3> below the first <p> and run again.

Code & diagrams

State pseudo-classes: hover, focus, active, checked, disabled New tab

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

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

checked: 1
disabled: 1
enabled buttons: 1
Structural pseudo-classes and nth-child formulas New tab

`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

Press Run to see the page.

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: 2
A zebra-striped table with a highlighted row New tab

Hover a row to highlight it. Change `even` to `odd`, or to `3n`, and predict which rows are striped before you run.

Preview

Press Run to see the page.

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.

terminal
$ press Tab in the preview
── what you'll see ──
Focus moves between the link, button and input, but nothing on screen shows where it is.

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.

terminal
$ the preview shows
── what you'll see ──
The last paragraph stays black.

Break #3

Link states in the wrong order

In the first example, move the a:hover rule above a:visited.

terminal
$ hover a visited link
── what you'll see ──
The link no longer turns red on hover once it has been 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.

  • ▸

    :visited is deliberately limited: only colour properties apply, the colour's alpha comes from the unvisited style, and getComputedStyle reports 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-child rules forces the browser to restyle every following sibling; a static class may be cheaper for very large, frequently changing lists.

  • ▸

    Selectors Level 4 says :empty should 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. 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. 2

    User-action states. :hover matches while a pointer (mouse) is over the element. :active matches while it is being pressed (mouse button down, or a tap). :focus matches the element that currently receives keyboard input (after a click on a field or pressing Tab). :focus-visible matches 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-visible and never remove the outline without a replacement: keyboard users need it to see where they are.

  3. 3

    Link and form states. :link matches unvisited links and :visited links the user has been to. For privacy, browsers only let :visited change colours (such as color, 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. :checked matches ticked checkboxes, selected radio buttons and selected <option>s. :disabled and :enabled match form controls by whether they can be used.

  4. 4

    Position among siblings (structural pseudo-classes). :first-child and :last-child match 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 accepts odd, even and formulas An+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. 5

    Of-type versions. :first-child counts all siblings, whatever their tag: p:first-child only matches a p that is the very first child. If a heading comes first, no p matches. :first-of-type, :last-of-type and :nth-of-type() count only siblings of the same tag: p:first-of-type is the first p among its siblings even if a heading comes before it.

  6. 6

    :empty matches 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

01

What is the difference between :focus and :focus-visible, and why does it matter for accessibility?

02

Explain li:nth-child(-n+3) and li:nth-child(n+4). What does each match?

03

What is the difference between p:first-child and p:first-of-type? Give a case where only one matches.

04

Why can't you change the font size of visited links?

05

In what order should you write link pseudo-classes, and why?

Practice

01

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.

02

Make every third card in a .grid of .card elements have a gold border, starting with the third.

03

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 .odd or .last classes), 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, :disabled and :enabled on form controls.

  • I can work out what an An+B formula matches.

  • I know when to use :first-of-type instead of :first-child.

  • I know what counts as :empty.

  • I never remove a focus outline without a visible replacement.