CSS: Pseudo-classes

Target elements based on their state, position, or relationship to other elements.

:hover and :active

:hover applies styles when the user moves their mouse over an element. :active applies while the element is being clicked or pressed. Both are commonly used on buttons and links.

:hover and :active

:hover and :active give visual feedback to the user. Always provide hover and active states on interactive elements like buttons and links.

  • :hovermouse is over the element
  • :activeelement is being clicked (held down)
  • :hover does not work on touch devices, use :focus-visible for keyboard users
  • Stack them in order: :link, :visited, :hover, :active (LoVe HAte mnemonic)

:hover and :active

Button changes colour on hover and shrinks slightly when clicked.

:focus

:focus applies when an element receives focus, either by clicking on it or by tabbing to it with the keyboard. It is critical for keyboard accessibility.

:focus

Never remove the default focus outline without replacing it with a visible alternative. Keyboard users depend on it to know which element is active.

  • :focuselement is focused via click or keyboard Tab
  • :focus-visiblefocus ring shows only for keyboard navigation, not mouse clicks (modern best practice)
  • :focus-withinapplies to a parent when any of its children are focused
  • Never use outline: none on :focus without providing a visible replacement
  • Use :focus-visible to avoid showing focus rings on mouse users while keeping them for keyboard users

:focus

A custom focus ring appears when the input or button receives keyboard focus.

:visited and :link

:link targets anchor elements that have not been visited yet. :visited targets links the user has already opened. Together they let you style links differently based on browsing history.

:link and :visited

For privacy reasons, :visited can only change color, background-color, border-color, and outline-color. JavaScript cannot read :visited state.

  • :linkunvisited anchor tag with an href
  • :visitedanchor tag the user has already followed
  • Correct order: :link then :visited (before :hover and :active)
  • Privacy restriction: only color-related properties work on :visited

:link and :visited

Unvisited links are blue; visited links turn grey.

:first-child and :last-child

:first-child selects an element that is the first child of its parent. :last-child selects the last child. These are useful for removing borders or margins from the first or last item in a list.

:first-child and :last-child

Use :first-child and :last-child to remove unwanted spacing or borders at the edges of a list or group without adding extra classes.

  • li:first-childthe first li inside its parent
  • li:last-childthe last li inside its parent
  • :first-of-typethe first element of that tag type inside its parent (ignores siblings of other types)
  • :last-of-typethe last element of that tag type inside its parent
  • Common pattern: add a border-bottom to all li items, then remove it from :last-child

:first-child and :last-child

Border removed from the last item; first item has a distinct background.

:nth-child() and :nth-of-type()

:nth-child() selects elements by their position among siblings. You can pass a number, the keywords odd / even, or a formula like 3n+1.

:nth-child()

:nth-child() counts all sibling elements regardless of type. :nth-of-type() counts only siblings of the same element type.

  • :nth-child(1)the first child
  • :nth-child(odd)children 1, 3, 5, 7 …
  • :nth-child(even)children 2, 4, 6, 8 …
  • :nth-child(3n)every third child: 3, 6, 9 …
  • :nth-child(3n+1)children 1, 4, 7, 10 …
  • :nth-of-type(2)the second element of that tag type inside its parent

:nth-child(even)

Alternating row colours using nth-child, a classic zebra-stripe table.

:not() Selector

:not(selector) selects every element that does not match the given selector. It is useful for applying styles to all items except one specific case.

:not()

:not() is a negation pseudo-class. It accepts any valid selector. Modern CSS also supports a comma-separated list inside :not() to exclude multiple selectors at once.

  • li:not(:last-child)all li items except the last one
  • input:not([type='submit'])all inputs except submit buttons
  • :not(.active)all elements without the active class
  • :not(.a, .b)excludes elements matching either .a or .b (modern CSS)
  • The specificity of :not() is determined by its argument, not by :not() itself

:not(:last-child)

Border-bottom added to all items except the last, no extra class needed.

:checked, :disabled, :enabled

These pseudo-classes target form elements based on their interactive state. They are essential for building custom-styled form controls that still respond to native HTML behaviour.

Form state pseudo-classes

These pseudo-classes reflect the live state of a form control. They update automatically when the user interacts with the form.

  • :checkedcheckbox or radio that is currently checked
  • :disabledinput, button, or select with the disabled attribute
  • :enabledinput or control that is not disabled
  • :read-onlyinput with the readonly attribute
  • :placeholder-showninput whose placeholder text is currently visible

:checked, :disabled

Checked checkbox gets a highlight; disabled input is visually greyed out.

:valid and :invalid

:valid and :invalid reflect whether an input's current value passes the HTML constraint validation rules (such as required, type="email", or minlength).

:valid and :invalid

:invalid fires immediately on page load for empty required fields. Use :not(:placeholder-shown):invalid to only show the error state after the user has started interacting.

  • :validinput value passes all validation constraints
  • :invalidinput value fails one or more constraints
  • :requiredinput has the required attribute
  • :optionalinput does not have the required attribute
  • input:not(:placeholder-shown):invalidinvalid only when the user has typed something

:valid and :invalid

Border turns green for a valid email and red for an invalid one.

:is(), :where(), :has() (Modern)

These are newer pseudo-classes that make selectors more powerful and concise. They are now well supported in all modern browsers.

:is(), :where(), :has()

These three pseudo-classes group selectors, reduce repetition, and unlock parent selection, previously impossible in CSS.

  • :is(h1, h2, h3)matches any of the listed selectors; takes the highest specificity of its arguments
  • :where(h1, h2, h3)same as :is() but always has zero specificity, safer for resets and utilities
  • :has(img)selects a parent that contains a matching child (the 'parent selector')
  • :has(:checked)selects a label when its checkbox is checked
  • :is() and :where() reduce repetitive selector lists; :has() enables patterns previously requiring JavaScript

:has(img)

The card that contains an image gets a different border colour via :has().

Knowledge Check

1. Which pseudo-class applies while the user hovers the mouse over an element?

2. What does :nth-child(2n) select?

3. Which pseudo-class selects an element only when it does NOT match a given selector?

4. What is the difference between :first-child and :first-of-type?

5. Which pseudo-class applies styles when an input's value passes HTML validation?

6. What does :has() allow you to do?