---
title: 'Pseudo-Classes and Pseudo-Elements'
source: 'https://academia.sh/en/courses/css-fundamentals/pseudo-classes-and-pseudo-elements'
course: 'Visual Presentation with CSS'
language: en
updated: '2026-08-17T18:09:17+00:00'
license: 'CC BY-SA 4.0'
---

# Pseudo-Classes and Pseudo-Elements

Pseudo-classes that select states not written in the document, nth-child arithmetic, logical selectors, and pseudo-elements that generate content absent from the tree.

Every selector so far has looked at something written in the document: element name,
class, id, attribute, position in the tree. There is also information that is not written
in the document. Whether a link is being hovered over, whether a form field has focus,
which position a row sits at — none of this appears in the markup, because none of it is a
property of the document.

This lesson defines two mechanisms: pseudo-classes, which select **states** not written in
the document, and pseudo-elements, which target **parts** not present in the document tree
at all. The two do different jobs, and the only difference in notation is the number of
colons.

## A Pseudo-Class Selects a State

A **pseudo-class** is written with a single colon and adds a condition to the selector
about the element's current state. It splits into two families.

The first family asks about **interaction state**: `:hover` matches while the pointer is
over the element, `:focus` while the element holds keyboard focus, `:active` while the
element is being activated, `:disabled` while a control is disabled, `:checked` while an
option is checked, `:invalid` while a field's value does not satisfy its constraints. None
of these states is written in the document; they come from user interaction or from
validation results.

The second family asks about **position in the tree**: `:first-child`, `:last-child`,
`:only-child`, `:empty`, `:root`, and `:nth-child()`, the head of a numbered family.

## nth-child Is an Arithmetic Expression

`:nth-child()` does not take a number, it takes a **formula**: `an+b`. Here `n` is an
integer starting at zero and increasing; every value the formula produces, as long as it
falls between 1 and the sibling count, matches a position.

The following program solves this formula and lists which positions match in a nine-row
measurement table.

```js
// nth-calc.mjs — solves the :nth-child(an+b) formula and lists which positions match
function parse(expr) {
  const s = expr.replace(/\s+/g, "").toLowerCase();
  if (s === "odd")  return { a: 2, b: 1 };
  if (s === "even") return { a: 2, b: 0 };
  const m = s.match(/^([+-]?\d*)n([+-]\d+)?$/);
  if (!m) return { a: 0, b: Number(s) };
  const aText = m[1];
  const a = aText === "" || aText === "+" ? 1 : aText === "-" ? -1 : Number(aText);
  return { a, b: m[2] ? Number(m[2]) : 0 };
}

// n = 0,1,2,... gives an+b; positions where 1 <= position <= total match
function matched(expr, total) {
  const { a, b } = parse(expr);
  const c = [];
  for (let position = 1; position <= total; position++) {
    if (a === 0) { if (position === b) c.push(position); continue; }
    const n = (position - b) / a;
    if (Number.isInteger(n) && n >= 0) c.push(position);
  }
  return { a, b, matched: c };
}

const TOTAL = 9; // nine rows in the measurement table
for (const expr of ["odd", "even", "3", "2n+1", "3n", "3n+1", "-n+3", "n+4", "5n-2"]) {
  const { a, b, matched: m } = matched(expr, TOTAL);
  const formula = `${a}n${b >= 0 ? "+" : ""}${b}`;
  console.log(`:nth-child(${expr.padEnd(5)}) = ${formula.padEnd(7)} -> ${m.join(" ") || "(empty)"}`);
}
```

```
:nth-child(odd  ) = 2n+1    -> 1 3 5 7 9
:nth-child(even ) = 2n+0    -> 2 4 6 8
:nth-child(3    ) = 0n+3    -> 3
:nth-child(2n+1 ) = 2n+1    -> 1 3 5 7 9
:nth-child(3n   ) = 3n+0    -> 3 6 9
:nth-child(3n+1 ) = 3n+1    -> 1 4 7
:nth-child(-n+3 ) = -1n+3   -> 1 2 3
:nth-child(n+4  ) = 1n+4    -> 4 5 6 7 8 9
:nth-child(5n-2 ) = 5n-2    -> 3 8
```

Three lines call for a further reading. `odd` and `even` are keywords that name the
formulas `2n+1` and `2n`; they are not a separate mechanism. `-n+3`, with its negative
coefficient, means "the first three": as `n` grows, the value shrinks, and the match ends
once it drops below 1. Its counterpart `n+4` means "from the fourth to the end." These two
are the way to select a range.

A warning: `:nth-child()` counts **all siblings**, regardless of type. If a section has a
heading followed by paragraphs, the first paragraph does not match `p:nth-child(1)` —
that position belongs to the heading. When counting by type is needed, `:nth-of-type()` is
written; it counts only siblings that carry the same element name.

## Logical Selectors

Four pseudo-classes take a selector list and perform a logical operation.

`:is()` returns elements that match **any one** of the selectors in the list; it shortens
long selectors. `:where()` performs the same match but contributes **zero** to
specificity — why this distinction matters is shown with numbers in the next lesson.

```css
:is(h1, h2, h3) + p { margin-block-start: 0.5em; }
```

This rule selects the same elements as writing three separate rules would.

`:not()` returns elements that match none of the items in the list:

```css
.measurement-table td:not(.name) { text-align: right; }
```

`:has()`, in turn, reverses the direction: it selects an element based on **its content**.
The selector `tr:has(td.missing)` returns rows that contain a missing cell. This is
something combinators cannot do; a combinator could only select the element on the right.

Whether a selector is recognized by implementations can be tested. Earlier lessons showed
that an unrecognized selector drops the entire rule; that also gives a way to test for it:

```css
@supports selector(:has(*)) {
  tr:has(td.missing) { background-color: #fbf4f4; }
}
```

The `@supports` block ignores its entire contents if the condition inside it is not met.
Writing a feature check is different from assuming a feature is supported: when the
condition is not met, what state the page is left in is decided by the author.

## A Pseudo-Element Targets a Part

A **pseudo-element** is written with two colons and targets a part that is not present in
the document tree.

`::first-line` selects a paragraph's first line, `::first-letter` its first letter. Neither
of these exists as an element in the document; the line break is a result of layout, and
the first line's extent changes when the box's width changes.

`::marker` targets a list item's bullet or number, `::placeholder` a form field's
placeholder text, `::selection` the text range the user has selected.

`::before` and `::after` are different: they do not select a part, they **generate** one.
Neither exists without the `content` property being written.

```css
td.missing::after { content: " —"; }
```

This rule appends a dash to the end of a missing-measurement cell's content. The generated
content is inside the element, not outside it: `::before` behaves like the element's first
child, `::after` like its last.

Generated content has two limits, and both are by design.

First, **it is not in the document**. Being selectable as text and copied, being indexed
by a search engine, or being conveyed to assistive technology is not guaranteed. For this
reason, no text carrying meaning is ever written here. The information "missing" should be
stated in the document with a class and, where needed, visible text; a pseudo-element
should only add a visual indicator.

Second, **it cannot be selected**. No other selector reaches inside a pseudo-element; the
generated content has no substructure of its own.

A note on notation: the double-colon form was defined to separate a pseudo-element from a
pseudo-class. The single-colon notation `:before` is also accepted for compatibility with
older implementations; this course uses the double colon to keep the distinction clear.

## The Station Page's Status Styles

```css
/* station.css — step 5: states and positions */
.measurement-table tbody tr:nth-child(odd) { background-color: #f5f7f8; }
.measurement-table tbody tr:last-child     { border-block-end: none; }
.measurement-table td:not(.name)           { text-align: right; }
.measurement-table td.missing::after       { content: " —"; color: #8a5b00; }

a:hover                { text-decoration-thickness: 2px; }
a:focus-visible        { outline: 3px solid #143a52; outline-offset: 2px; }
input:invalid          { border-color: #8a1c1c; }
input:disabled         { color: #6b767f; }
```

The first two rules of the second group carry a distinction. `:hover` is a state that only
occurs on pointer devices; a user navigating by keyboard never reaches it. This is why
every hint given with `:hover` should have a focus counterpart.

`:focus-visible` is used instead of `:focus` because the two return different sets:
`:focus` also matches when a field is clicked on with a pointer, `:focus-visible` matches
in cases where a focus indicator is needed. Removing the focus outline entirely (writing
`outline: none`) makes keyboard navigation impossible; if the outline is going to be
removed, a visible replacement should be put in its place.

## Summary

- A pseudo-class is written with a single colon and selects a state not written in the
  document; it splits into two families — interaction state and position in the tree.
- `:nth-child()` takes an `an+b` formula; `n` starts at zero, and the valid positions the
  formula produces match. `odd` and `even` are named forms of this formula.
- `:nth-child()` counts all siblings, `:nth-of-type()` only those of the same type; the
  difference shows up around interposed elements like a heading.
- `:is()`, `:where()`, `:not()`, and `:has()` take a selector list; `:has()` selects an
  element by its content and provides the reverse direction of a combinator.
- A pseudo-element is written with two colons; `::before` and `::after` generate content
  not present in the document. This content is not reliably reachable as text, so it
  should carry only a visual indicator.

## Next Step

More than one rule can now match the same element: a `td` by one rule, `.missing` by
another, `.measurement-table td:not(.name)` by a third. If all three write to the
`text-align` property, which one wins? The next lesson answers this question not with
intuition but with a countable ordering: it computes the specificity triple and applies
the cascade's steps in order.
