Pseudo-classes and pseudo-elements

Style interactive states, select elements by position, and add decorative content with ::before and ::after.

  • Beginner
  • 7 min read
  • Updated 17 Oct 2026

What you will learn

  • Style interactive states
  • Write visible focus styles with :focus-visible
  • Select elements by position with :nth-child
  • Add generated content safely

A pseudo-class selects an element in a particular state or position, such as "being hovered" or "the first child". A pseudo-element selects a part of an element, or adds content that is not in the HTML. Pseudo-classes use one colon; pseudo-elements use two.

User action states

CSS
a {
    color: #0b5394;
}

a:hover {
    color: #073763;
    text-decoration-thickness: 0.15em;
}

.button:active {
    transform: translateY(1px);
}
Pseudo-classMatches when
:hoverA pointer is over the element
:activeThe element is being pressed
:focusThe element has keyboard, pointer or programmatic focus
:focus-visibleThe element has focus and the browser judges a focus ring should show
:focus-withinThe element or anything inside it has focus
:visitedA link has been visited (only colour changes are allowed, for privacy)

Touchscreens have no real hover. Never hide essential information or actions behind :hover alone. Make sure everything also works with a tap, and with a keyboard.

Focus styles with :focus-visible

Keyboard users rely on a visible focus indicator to know where they are. Browsers draw one by default, and removing it with outline: none is one of the most common accessibility bugs on the web.

:focus-visible solves the old complaint that focus rings appeared on mouse clicks. Browsers apply it when focus should be shown, typically for keyboard navigation and for text inputs, but usually not when a button is clicked with a mouse.

CSS
:focus-visible {
    outline: 3px solid #f0b429;
    outline-offset: 2px;
}

.button:focus-visible {
    outline-color: #102a43;
}

Use outline, not border, so the indicator does not shift the layout. Make it thick and high-contrast against the surrounding colours. The Web Accessibility Essentials course covers focus order and focus visibility requirements in detail.

Do not write :focus { outline: none; } without an equally clear replacement. If a designer asks for no focus rings, design a better one rather than removing it.

:focus-within

CSS
.search:focus-within {
    box-shadow: 0 0 0 3px #f0b429;
}

The whole search box highlights when its input has focus.

Form states

CSS
input:disabled {
    opacity: 0.6;
    cursor: not-allowed;
}

input:checked + label {
    font-weight: 600;
}

input:required {
    border-left: 3px solid #0b5394;
}

input:user-invalid {
    border-color: #c53030;
}

:checked, :disabled, :required and :placeholder-shown reflect form control state. :invalid matches as soon as the page loads, so an empty required field looks like an error before the user has typed anything. :user-invalid waits until the user has interacted with the field, which is much friendlier. It is a newer pseudo-class, so check its Baseline status, and never use a red border as the only sign of an error.

Structural pseudo-classes

These select elements by their position among siblings:

CSS
li:first-child { font-weight: 700; }
li:last-child { border-bottom: none; }
li:only-child { list-style: none; }

:nth-child()

:nth-child() takes a number, a keyword or a formula An+B:

CSS
tr:nth-child(odd) {
    background-color: #f0f4f8;   /* zebra stripes */
}

li:nth-child(3) {
    color: #c53030;              /* the third item */
}

li:nth-child(3n) {
    margin-inline-end: 0;        /* every third item: 3, 6, 9... */
}

li:nth-child(-n + 3) {
    font-weight: 700;            /* the first three */
}

li:nth-last-child(2) {
    color: #486581;              /* second from the end */
}
ExpressionMatches
odd or 2n+11, 3, 5…
even or 2n2, 4, 6…
3n3, 6, 9…
n+44, 5, 6…
-n+31, 2, 3

:nth-child() counts all siblings, whatever their type. :nth-of-type() counts only siblings of the same element type. If a <h2> sits before your paragraphs, p:nth-child(1) matches nothing, while p:nth-of-type(1) matches the first paragraph.

:not()

CSS
.nav-link:not(.is-current) {
    opacity: 0.85;
}

li:not(:last-child) {
    border-bottom: 1px solid #d9e2ec;
}

:not() excludes matches. It can take a list of selectors: :not(.a, .b). Its specificity is that of its most specific argument.

Other useful pseudo-classes

  • :target matches the element whose id is in the URL fragment, such as #pricing. Useful to highlight the section a link jumped to.
  • :empty matches elements with no child elements and no text. In current browsers even a single space counts as content, so the element no longer matches.
  • :root matches the <html> element, and is where global custom properties usually go.

Pseudo-elements

::before and ::after

These insert a box as the first or last child of an element. They need the content property, even if it is empty:

CSS
.required-label::after {
    content: " *";
    color: #c53030;
}

.callout {
    position: relative;
    padding-inline-start: 1.5rem;
}

.callout::before {
    content: "";
    position: absolute;
    inset-block: 0;
    inset-inline-start: 0;
    width: 4px;
    background-color: #0b5394;
}

The empty-string version is very common: a decorative bar, an arrow, an overlay. Positioning is explained in a later lesson.

Generated content is not reliably available to everyone. Some screen readers announce it and some do not, and it cannot be selected or copied. Use ::before and ::after for decoration only. In the .required-label example, also mark the field with the required attribute and explain the asterisk in text, so the meaning does not depend on CSS.

::before and ::after do not work on replaced elements such as <img> and <input>, because those have no content box to insert into.

Other pseudo-elements

CSS
::selection {
    background-color: #f0b429;
    color: #102a43;
}

input::placeholder {
    color: #627d98;
}

li::marker {
    color: #0b5394;
}

.article p:first-of-type::first-letter {
    font-size: 3em;
    float: left;
    line-height: 1;
}
  • ::selection styles text the user highlights.
  • ::placeholder styles placeholder text. Keep it readable, and never use placeholders instead of labels.
  • ::marker styles list bullets and numbers.
  • ::first-letter and ::first-line style the start of a block of text.

Specificity

Pseudo-classes count like classes, (0, 1, 0). Pseudo-elements count like types, (0, 0, 1). So a:hover scores (0, 1, 1) and p::first-line scores (0, 0, 2).

Testing states in DevTools

Hovering away from an element to inspect it loses the hover state. In the Styles pane, click :hov to force states such as :hover, :focus, :focus-visible and :active on the selected element. Generated content appears in the Elements panel as ::before and ::after nodes you can select.

Summary

  • Pseudo-classes (:hover, :checked) select states and positions; pseudo-elements (::before) select parts or add content.
  • Style focus with :focus-visible and outline; never remove focus indicators without a replacement.
  • :nth-child() counts all siblings; :nth-of-type() counts siblings of the same type.
  • ::before and ::after need content and are for decoration only.
  • Force states with the :hov button in DevTools.