A pseudo-class is a keyword added to a CSS selector, written with a single colon, that matches elements based on state, position in the document tree or relationship to other elements rather than on their name, class or attributes alone. Examples include :hover for an element under the pointer, :checked for a selected checkbox and :nth-child(2n) for every even child.
In CSS, pseudo-classes let authors style conditions that cannot be expressed in markup, because they change with user interaction, form validity, navigation history or document structure. They are defined in the Selectors specifications, currently Selectors Level 4, and they are among the most frequently used tools for interactive, accessible and maintainable styling.
How pseudo-classes match elements
A pseudo-class acts as a filter on a compound selector. In a.button:hover, the browser first finds anchor elements with the class button, then keeps only those currently in the hover state. Because states change continuously, the browser re-evaluates matching whenever the relevant condition changes, for example when focus moves or a form field becomes invalid.
Some pseudo-classes are simple keywords such as :disabled, while functional pseudo-classes take an argument in parentheses, for example :nth-child(3n + 1), :not(.hidden) or :lang(de). Pseudo-classes can be chained, so input:required:invalid matches only required fields that currently fail validation.
Pseudo-classes differ in where their information comes from. Tree-structural ones read only the document, so they behave like ordinary selectors that update when the DOM changes. Input pseudo-classes read the live state of form controls, including validation constraints such as required, pattern or min. User action and location pseudo-classes depend on the browser and the user, which is why their exact behavior, for example when :focus-visible applies, is partly left to browser heuristics rather than fixed rules.
Main families of pseudo-classes
Pseudo-classes fall into several families according to the kind of condition they test. The table lists the most important groups with representative members.
| Family | Examples | Condition tested |
|---|---|---|
| User action | :hover, :active, :focus, :focus-visible | Pointer, keyboard and activation state |
| Location | :link, :visited, :target | Link history and URL fragment |
| Tree-structural | :first-child, :nth-of-type(), :empty, :root | Position among siblings and in the tree |
| Input | :checked, :disabled, :invalid, :placeholder-shown | Form control state and validity |
| Logical | :not(), :is(), :where(), :has() | Combinations of other selectors |
| Linguistic and other | :lang(), :dir(), :defined | Language, direction, custom element status |
The relational `:has()` pseudo-class, supported in all major browsers since late 2023, matches an element if it contains, or is followed by, something matching its argument. It enables patterns long called parent selectors, such as styling a card differently when it contains an image, or a form group when one of its fields is invalid.
Pseudo-classes and specificity
Each simple pseudo-class adds the same specificity as a class selector, one point in the middle column of the (ID, class, type) triple. The logical pseudo-classes follow special rules, which makes them powerful tools for controlling the CSS cascade.
| Selector | Specificity contribution | Practical consequence |
|---|---|---|
| `:hover`, `:checked` and similar | Same as one class | Overrides plain type selectors |
| `:is()` | Most specific argument | An ID inside raises the whole selector |
| `:not()` | Most specific argument | Negation can add unexpected weight |
| `:has()` | Most specific argument | Relational styling can win the cascade |
| `:where()` | Always zero | Ideal for easily overridable defaults |
Library and design-system authors often wrap base styles in `:where()` so that any consumer rule overrides them without escalating specificity. Conversely, :is(#main, .content) a carries the weight of an ID even when it matches through the class, a common source of surprising overrides.
Pseudo-class compared with pseudo-element
A pseudo-class selects an existing element in a particular condition, while a pseudo-element targets a part of an element or generated content that does not exist as a separate node in the DOM. Modern syntax uses one colon for pseudo-classes and two colons for pseudo-elements.
| Aspect | Pseudo-class | Pseudo-element |
|---|---|---|
| Syntax | Single colon, as in :hover | Double colon, as in ::before |
| What it selects | Whole element in a state | Part of an element or generated box |
| Number per selector | Several can be chained | Generally one, at the end |
| Typical examples | :focus-visible, :nth-child() | ::before, ::placeholder, ::marker |
| Combination | Can precede a pseudo-element | Example: a:hover::after |
Pitfalls and good practice
Link states must follow the LVHA order, :link, :visited, :hover, :active, because rules of equal specificity are resolved by source order and a later state would otherwise mask an earlier one. For privacy reasons, :visited styling is limited to color-related properties, and scripts cannot detect whether a link was visited.
- Prefer `:focus-visible` over removing outlines: it shows focus indicators for keyboard users without showing them on mouse clicks.
- Do not rely on `:hover` alone: touch screens have no true hover, so essential content must also be reachable by tap and keyboard.
- Use `:user-invalid` for validation styling: unlike
:invalid, it waits until the user has interacted, avoiding red fields on page load. - Remember that `:empty` counts whitespace: an element containing a space or line break is not empty in most implementations.
- Watch `:has()` performance: broad selectors such as
body:has(...)on very large documents can force costly style recalculation.
Used well, pseudo-classes remove the need for many JavaScript-managed classes, keep state styling declarative and improve accessibility through consistent focus and validation feedback. Used carelessly, they can create specificity conflicts and interactions that only work with a mouse.
The idea in brief
A pseudo-class lets a website change how something looks depending on its situation. A button can change color when the mouse is over it. A form field can turn red when the entry is wrong. Every second row in a table can get a light background.
Think of it as a rule that says “when this happens, look like this”. The page does not need extra labels for each case. The browser notices the situation by itself and applies the matching style.
A practical scenario
A team building a sign-up form wants clear validation without extra scripts. They style fields with input:user-invalid to show a red border and an error icon only after the user has typed and left the field, and input:user-valid for a subtle green check. The submit button uses :disabled styling while the form is incomplete.
To highlight a whole field group, they use .field:has(input:user-invalid) to color its label and helper text, and :focus-visible to draw a 3 px focus ring for keyboard users. The resulting form needs no JavaScript for visual validation states, behaves consistently with keyboard and touch input, and passes the team’s accessibility review for visible focus and error identification.