ScreenshotNeo

BlogGuides

CSS Pseudo-Classes: Selectors and Examples

Learn how CSS pseudo-classes match elements by state, position, and relationships, with practical selectors, specificity rules, accessible focus styles, and examples.

By the ScreenshotNeo team4 October 202612 min read

CSS pseudo-classes let you style an element when it matches a condition, such as being hovered, focused, checked, first among its siblings, or related to another element. They begin with a single colon: :hover, :checked, :nth-child(2). Use them to respond to browser and document state without adding a class for every condition.

This guide covers common pseudo-classes, runnable examples, structural formulas, relationship selectors, specificity, accessibility, and troubleshooting. For the normative definitions and selector rules, see the W3C Selectors Level 4 specification; for a broader index, see MDN’s pseudo-classes reference.

1. What is a CSS pseudo-class?

A pseudo-class is a selector that matches an element based on information that may not be represented by an attribute or class in the document tree. That information can be interaction state, form state, location in the document, or a relationship to other elements. The matched element is the selector’s anchor.

button:hover {
  background: #174ea6;
}

input:checked {
  accent-color: seagreen;
}

Some pseudo-classes are simple names, such as :hover. Functional pseudo-classes take arguments in parentheses, such as :nth-child(2n), :not(.muted), and :has(> img).

Pseudo-class vs pseudo-element

A pseudo-class matches an element in a state or context. A pseudo-element represents a particular part of an element. Modern CSS uses one colon for pseudo-classes and two for pseudo-elements:

a:hover { text-decoration: underline; } /* pseudo-class */
p::before { content: "→ "; }          /* pseudo-element */

Use the double-colon spelling for pseudo-elements such as ::before, ::after, ::first-line, and ::first-letter. The older single-colon spelling for those legacy pseudo-elements is retained for compatibility, but new code should use double colons.

2. How to run the examples

The following is a complete HTML file. Save it as pseudo-classes.html and open it in a browser. It demonstrates pointer states, keyboard focus, structural selectors, form state, and :has().

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>CSS pseudo-class examples</title>
  <style>
    :root {
      font-family: system-ui, sans-serif;
      color: #172033;
      background: #f6f8fc;
    }
    body { max-width: 48rem; margin: 2rem auto; padding: 0 1rem; }
    .demo, li, .field { background: white; border: 1px solid #ccd4e0; border-radius: .5rem; padding: .75rem; }
    .demo-link:hover { text-decoration-thickness: .15em; }
    .demo-link:focus-visible, button:focus-visible, input:focus-visible {
      outline: 3px solid #0879d1;
      outline-offset: 3px;
    }
    .field:focus-within { border-color: #0879d1; }
    li:nth-child(even) { background: #eaf1fb; }
    li:first-of-type { font-weight: 700; }
    input:required:invalid { border-color: firebrick; }
    input:required:valid { border-color: seagreen; }
    .card:has(> img) { display: grid; gap: .5rem; }
    .card img { max-width: 8rem; }
    :target { background: #fff2bd; scroll-margin-top: 1rem; }
  </style>
</head>
<body>
  <h1>Pseudo-class examples</h1>
  <p><a class="demo-link" href="#details">Hover or keyboard-focus this link</a></p>
  <form>
    <label class="field">Email
      <input type="email" required placeholder="name@example.com">
    </label>
    <p><input id="updates" type="checkbox"> <label for="updates">Receive updates</label></p>
    <button type="submit">Submit</button>
  </form>
  <h2>Sibling positions</h2>
  <ul><li>One</li><li>Two</li><li>Three</li><li>Four</li></ul>
  <article class="card"><img alt="A sample thumbnail" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='80'%3E%3Crect width='100%25' height='100%25' fill='%23aac8ef'/%3E%3C/svg%3E"><span>Card with an image</span></article>
  <p><a href="#details">Jump to details</a></p>
  <p id="details" class="demo">The URL fragment makes this element match :target.</p>
</body>
</html>

3. Interaction and focus pseudo-classes

Interaction pseudo-classes change as the user interacts. Their states can overlap: a link can be hovered, focused, and active at different moments, and some combinations can coexist. Write selectors for the state you intend to style rather than assuming these states are mutually exclusive.

Selector Matches Typical use
:hover An element designated by a pointing device Pointer feedback; never make it the only way to reveal important content.
:active An element being activated Brief pressed or selected feedback.
:focus An element that currently has input focus Focus styling when all focused interactions should be visible.
:focus-visible An element for which the browser determines a visible focus indicator is appropriate Keyboard-friendly focus indication without necessarily showing the same ring after every pointer interaction.
:focus-within An element that is focused or contains a focused descendant Highlighting a form row, menu, or composite control while a child control has focus.
/* Pointer feedback */
a:hover { text-decoration-thickness: 0.15em; }

/* Pressed feedback */
button:active { transform: translateY(1px); }

/* Keep a visible keyboard focus indicator */
.button:focus-visible {
  outline: 3px solid deepskyblue;
  outline-offset: 3px;
}

/* Fallback for browsers without :focus-visible support */
@supports not selector(:focus-visible) {
  .button:focus {
    outline: 3px solid deepskyblue;
    outline-offset: 3px;
  }
}

/* Indicate which form group contains the active control */
.field:focus-within { border-color: steelblue; }

:focus-visible is determined by browser heuristics, often based on input method and context. Preserve an obvious focus indication for keyboard users. A pointer user may not see the same ring, but the browser decides when the pseudo-class matches. The feature-detection fallback above is documented by MDN’s :focus-visible guide.

4. Structural pseudo-classes and sibling counting

Structural pseudo-classes match an element by its position among siblings. The crucial distinction is which sibling set is counted.

Selector Sibling set counted Example meaning
:first-child, :last-child All element siblings First or last element child of its parent.
:first-of-type, :last-of-type Siblings with the same element type First paragraph among sibling paragraphs.
:nth-child() All element siblings Element at a calculated position from the start.
:nth-of-type() Siblings of the same element type Element at a calculated position within its type.
:nth-last-child(), :nth-last-of-type() Same respective sets, counted from the end Positions near the end of a list or group.

Understand the An+B formula

:nth-child(An+B) matches positions numbered by substituting non-negative integers for n, keeping positive positions. n starts at zero. Common shortcuts include:

Formula Positions matched Example
odd or 2n+1 1, 3, 5, 7, … li:nth-child(odd)
even or 2n 2, 4, 6, 8, … tr:nth-child(even)
3n 3, 6, 9, 12, … .tile:nth-child(3n)
3n + 1 1, 4, 7, 10, … li:nth-child(3n + 1)
-n + 3 1, 2, 3 li:nth-child(-n + 3) selects the first three.
5 Only position 5 li:nth-child(5)
/* Zebra striping across all list-item siblings */
li:nth-child(even) { background: #f5f5f5; }

/* Every third item, beginning with the first */
li:nth-child(3n + 1) { font-weight: 700; }

/* First paragraph among its element type */
p:first-of-type { margin-top: 0; }

/* The second .item among siblings that match .item */
.item:nth-child(2 of .item) { border-color: royalblue; }

The filtered form :nth-child(An+B of <selector-list>) first limits the sibling set to elements matching the selector list, then counts within that set. For example, :nth-child(2 of .item) means the second sibling matching .item, even if other sibling elements occur before it. This filter also affects specificity; see the specificity section and the W3C specification.

5. Form and input state pseudo-classes

Form pseudo-classes reflect native control state and HTML constraint validation. They let the browser’s state drive styling without duplicating it in application classes.

Selector Use
:checked Checked checkbox, radio button, or selected option.
:disabled, :enabled Controls that are disabled or enabled.
:required, :optional Controls with or without the required constraint.
:valid, :invalid Controls or forms that satisfy or fail applicable constraints.
:read-only, :read-write Elements users can or cannot edit.
:placeholder-shown An input currently displaying its placeholder.
input:required:invalid { border-color: firebrick; }
input:required:valid { border-color: seagreen; }
input:disabled { opacity: 0.6; }
input:checked + label { font-weight: 700; }
input:placeholder-shown { background: #f7f9fc; }

Use these styles as supplemental feedback, not as the only way to communicate meaning. Do not rely on color alone for an error or checked state; provide clear labels and useful error text. Also consider when validation should appear: a required field may be invalid before the user has had a chance to fill it in.

6. Logical and relational pseudo-classes

Logical pseudo-classes combine, include, or exclude selector conditions. :has() matches an anchor according to a related element described by a relative selector.

Selector Meaning Specificity behavior
:is(A, B) Matches if the element matches any argument. Uses the most specific argument.
:where(A, B) Matches if the element matches any argument. Always contributes zero specificity.
:not(A) Matches if the element does not match its argument. Uses the most specific argument.
:has(relative-selector) Matches an anchor when the described relationship exists. Uses the most specific argument.
/* Any heading in the list shares the rule */
:is(h1, h2, h3) { font-family: Georgia, serif; }

/* Group selectors without adding specificity */
:where(header, footer) a { color: inherit; }

/* Exclude muted items */
.card:not(.muted) { opacity: 1; }

/* A link with a direct image child */
a:has(> img) { display: inline-block; }

/* A section that contains an h2 somewhere inside */
section:has(h2) { padding-block-start: 1rem; }

:has() is sometimes called a parent selector, but it is more precise to read the relative selector. In .card:has(> img), the anchor is the card and the argument asks whether it has a direct image child. In section:has(h2), the relationship allows a descendant heading. The selector cannot be nested inside another :has(). Check compatibility against the browsers your project supports; the dossier sources do not provide a complete dated version matrix for every modern selector.

7. Location and document-state pseudo-classes

These selectors respond to the document root, URL fragment, or the element’s child-content state.

/* Normally the html element in an HTML document */
:root { color-scheme: light dark; }

/* Element whose ID matches the URL fragment */
:target {
  scroll-margin-top: 5rem;
  background: #fff4c2;
}

/* An element with no child content under the selector rules */
.notice:empty { display: none; }

:root is useful for document-level custom properties and settings. :target can highlight the destination of an in-page link. :empty has a specific definition about child content; whitespace text nodes can mean an element is not empty, so verify the actual markup if the rule does not match.

8. Specificity: why a pseudo-class rule wins

Specificity helps the cascade decide between matching rules when origin, importance, and cascade layer do not already decide the result. A regular pseudo-class contributes class-level specificity. Think of an ID, class-like selector, and type selector as the three specificity columns.

Selector Specificity Reason
button:hover 0-1-1 One pseudo-class and one type selector.
#panel :focus 1-1-0 One ID and one pseudo-class.
:where(.card) a 0-0-1 :where() adds zero; the type selector adds one.
:is(.card, #featured) a 1-0-1 :is() uses its most specific argument, here the ID.
.item:nth-child(2 of #list .item) 1-3-0 The pseudo-class and the most specific filtered selector contribute.

:is(), :not(), and :has() take specificity from the most specific selector in their argument list. :where() always contributes zero. The of selector list in :nth-child() or :nth-last-child() contributes the most specific selector in that list in addition to the pseudo-class. If an override surprises you, inspect the full selector and function arguments, not just the visible pseudo-class name.

9. Accessibility and authoring checklist

  • Keep keyboard focus visible. Use :focus-visible where appropriate and retain a fallback if supporting environments that lack it.
  • Do not make hover the only way to discover controls or important content; touch and keyboard users may not have a pointer hover state.
  • Pair color changes with text, shape, iconography, or other cues when communicating checked or invalid state.
  • Use native form semantics such as required and input types so validity pseudo-classes reflect meaningful constraints.
  • Use :where() when selector grouping should not make later overrides harder.
  • Check target browser support before relying on newer functional selectors such as filtered :nth-child() or :has().
  • Keep selectors understandable. A short selector with an obvious anchor and relationship is easier to maintain than a clever chain.

10. Troubleshooting common pseudo-class problems

Symptom Likely cause Fix
:hover styling does not appear The device has no hover-capable pointer, the pointer is not over the selected anchor, or another rule wins. Test with a mouse or trackpad, inspect the matched element, and check the cascade. Keep the interaction usable without hover.
Keyboard focus has no visible ring The stylesheet removed the outline, the selector targets a different element, or a later rule overrides it. Tab through the page, inspect computed styles, and add a clear :focus-visible style. Avoid removing outlines without a visible replacement.
:nth-child() selects the wrong row It counts every element sibling, including other element types, rather than only the target type. Use :nth-of-type() when counting same-type siblings, or :nth-child(An+B of .item) to filter the count.
:first-child matches nothing The desired element is not the first element child of its parent. Inspect the parent’s actual element children. Use :first-of-type if you mean first of that tag type.
A form field looks invalid immediately A required field is empty, so it fails constraint validation before interaction. Choose when to show validation feedback, or style based on a form state that fits the interaction. Keep explicit error text available.
:has() rule is ignored The target browser may not support the selector, the relative relationship is wrong, or the child is not where expected. Check the project’s browser targets and DOM structure. Simplify the argument, such as :has(> img) for a direct child, and provide a fallback where needed.
A grouped selector is unexpectedly hard to override :is(), :not(), or :has() inherits the most specific argument’s specificity. Review argument specificity. Use :where() for zero-specificity grouping when suitable.
:empty does not match The element has child content, potentially including whitespace text. Inspect its DOM children and remove unintended content or use a selector that matches the intended condition.

11. Performance, reliability, and maintenance

Pseudo-classes are evaluated as part of selector matching when style is recalculated. For ordinary interface styles, choose selectors for clarity and correct behavior first. Large or complex relational selectors can make the intended relationship harder to see and may increase work during style recalculation, so keep the anchor and relationship narrow, especially for state that changes frequently.

For reliable styling, test the states that trigger the selector, not only the initial page load: keyboard focus, pointer interaction, form validity before and after input, list changes, fragment navigation, and the target browser set. Dynamic pages can change sibling order or add content, which changes structural matches. No performance benchmarks are asserted here; inspect your own page if you suspect style recalculation is a bottleneck.

12. Capture pseudo-class states for review

When reviewing a page, you may need screenshots of a default state, a focused control, a validation result, or a highlighted target. You can inspect these states manually in the browser, or use a screenshot API after configuring the relevant state and waiting for the page to settle.

DIY browser workflow

  1. Open the page in a browser and identify the element and condition you need to review.
  2. Trigger the state. Use Tab for keyboard focus, hover with a pointer, check a control, or navigate to a URL fragment.
  3. Wait for animations, validation updates, and any application rendering to finish.
  4. Capture the viewport or full page with your browser’s screenshot facility or automation setup.
  5. Repeat for the important states and viewport sizes. A screenshot of the initial state does not prove the focused or invalid state is styled correctly.

Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server for developers. Its one-call screenshot request can capture a page as PNG, JPEG, WebP, or PDF. For a page whose pseudo-class state is produced by ordinary interaction, configure the state in your page or use its custom JavaScript and click options before capture. See the ScreenshotNeo API documentation for request options.

curl -G "https://api.screenshotneo.com/v1/shot" \
  -d access_key=YOUR_API_KEY \
  --data-urlencode url=https://example.com \
  -o shot.webp

ScreenshotNeo removes cookie banners, newsletter popups, and chat widgets before the shot; each cleanup step can be turned off. Bot checks, blank pages, and failed loads are never billed, and response headers report the page verdict and billing status. Its MCP server lets AI agents using Claude, Cursor, or another MCP client take screenshots. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000. Sign up for 1,000 free screenshots a month, with no card required.

13. Short FAQ

Can I use more than one pseudo-class on an element?

Yes. Chain them when the element must satisfy every condition, as in input:required:invalid. The states may also overlap, as with a focused and hovered link.

Does :has() select only parents?

No. It matches its anchor based on the relative selector argument, which can describe child, descendant, or other relative relationships. Read the argument to see exactly what relationship is tested.

Should I use :focus or :focus-visible?

Use :focus when every focused state should receive the style. Use :focus-visible when you want the browser to determine when a visible focus indicator is appropriate. In either case, keep keyboard focus visible.

Are pseudo-classes JavaScript?

No. They are CSS selectors. The browser updates matching when relevant state changes; JavaScript is only needed when the behavior or state itself requires application logic.

Sources