---
title: "CSS Selectors Cheatsheet"
slug: "css-selectors"
category: "cheatsheets"
tags: ["cheatsheets", "css", "selectors", "specificity", "frontend"]
status: "stable"
last_updated: 2026-05-14
summary: "Every CSS selector type with an example, specificity score, and use case, including :has, :is, :where, :not, and :nth-child."
related:
  [
    "[[frontend/css]]",
    "[[frontend/css-has-selector]]",
    "[[frontend/css-cascade-layers]]",
    "[[cheatsheets/http-status-codes]]",
    "[[cheatsheets/index|Cheatsheets]]",
  ]
---

## Overview

Specificity decides which rule wins. This card lists every selector type, what it matches, and the specificity score it contributes. The score is a tuple (inline, IDs, classes/attrs/pseudo-classes, types/pseudo-elements); higher tuples win. For deeper coverage on cascade layers and modern selectors, see [[frontend/css-cascade-layers]] and [[frontend/css-has-selector]].

## Basic selectors

The selectors a CSS file leans on for most rules.

| Selector             | Example           | Matches                           | Specificity |
| -------------------- | ----------------- | --------------------------------- | ----------- |
| Type                 | `p`               | Every `<p>` element.              | (0,0,0,1)   |
| Class                | `.btn`            | Elements with class `btn`.        | (0,0,1,0)   |
| ID                   | `#header`         | The element with id `header`.     | (0,1,0,0)   |
| Universal            | `*`               | Every element.                    | (0,0,0,0)   |
| Attribute (present)  | `[disabled]`      | Elements that have the attribute. | (0,0,1,0)   |
| Attribute (equals)   | `[type="email"]`  | Exact match.                      | (0,0,1,0)   |
| Attribute (prefix)   | `[href^="https"]` | Starts with.                      | (0,0,1,0)   |
| Attribute (suffix)   | `[src$=".svg"]`   | Ends with.                        | (0,0,1,0)   |
| Attribute (contains) | `[class*="card"]` | Substring match.                  | (0,0,1,0)   |

Reach for IDs only when no class will do. The (0,1,0,0) score is hard to override without `!important` or layer stacking.

## Combinators

How selectors compose left to right.

| Combinator       | Example   | Matches                                     |
| ---------------- | --------- | ------------------------------------------- |
| Descendant       | `nav a`   | Any `a` inside a `nav`, at any depth.       |
| Child            | `ul > li` | Direct child `li` of `ul`.                  |
| Adjacent sibling | `h2 + p`  | The `p` immediately after an `h2`.          |
| General sibling  | `h2 ~ p`  | Every `p` after an `h2` in the same parent. |

The child combinator is faster than descendant for the browser; both are fast enough for any real-world page.

## Pseudo-classes (state)

State-based selectors react to user input, structure, or form data.

| Pseudo-class          | Matches                                  | Specificity |
| --------------------- | ---------------------------------------- | ----------- |
| `:hover`              | Mouse is over the element.               | (0,0,1,0)   |
| `:focus`              | Element has keyboard focus.              | (0,0,1,0)   |
| `:focus-visible`      | Focus from keyboard, not click.          | (0,0,1,0)   |
| `:active`             | Element is being activated.              | (0,0,1,0)   |
| `:checked`            | Checkbox or radio is checked.            | (0,0,1,0)   |
| `:disabled`           | Form control is disabled.                | (0,0,1,0)   |
| `:required`           | Form control has `required`.             | (0,0,1,0)   |
| `:valid` / `:invalid` | Form value passes / fails validation.    | (0,0,1,0)   |
| `:empty`              | Element has no children, including text. | (0,0,1,0)   |
| `:target`             | Element matches the URL fragment.        | (0,0,1,0)   |
| `:root`               | The document root (usually `<html>`).    | (0,0,1,0)   |
| `:link` / `:visited`  | Anchor link state.                       | (0,0,1,0)   |

Use `:focus-visible` for focus rings; it skips the ring on mouse clicks while keeping it for keyboard users.

## Pseudo-classes (structural)

Position within a parent. The `n` is a 1-based index.

| Pseudo-class         | Matches                                      |
| -------------------- | -------------------------------------------- |
| `:first-child`       | First child of its parent.                   |
| `:last-child`        | Last child of its parent.                    |
| `:only-child`        | Sole child of its parent.                    |
| `:first-of-type`     | First element of its type among siblings.    |
| `:last-of-type`      | Last element of its type among siblings.     |
| `:nth-child(2)`      | Second child of its parent.                  |
| `:nth-child(odd)`    | 1st, 3rd, 5th, ...                           |
| `:nth-child(2n+1)`   | Same as odd.                                 |
| `:nth-child(3n)`     | Every third child.                           |
| `:nth-last-child(1)` | Counted from the end; same as `:last-child`. |
| `:nth-of-type(2)`    | Second of its type.                          |

`:nth-child` counts siblings of any type; `:nth-of-type` counts siblings of the same type. They are not interchangeable.

## Modern logical pseudo-classes

The selectors that changed how complex rules are written.

| Selector      | Example               | What it does                                               | Specificity     |
| ------------- | --------------------- | ---------------------------------------------------------- | --------------- |
| `:is(...)`    | `:is(h1, h2, h3) + p` | List shorthand; takes the highest specificity inside.      | Highest in list |
| `:where(...)` | `:where(h1, h2) + p`  | Like `:is` but contributes (0,0,0,0).                      | (0,0,0,0)       |
| `:not(...)`   | `li:not(.skip)`       | Negation; takes the highest specificity inside.            | Highest in list |
| `:has(...)`   | `article:has(img)`    | Parent selector; matches `article` that contains an `img`. | Highest in list |

`:where` is the right tool for resets and base styles; its zero specificity will not fight component CSS.

## Pseudo-elements

Pseudo-elements address parts of an element that are not separate elements.

| Pseudo-element   | Targets                                         | Specificity |
| ---------------- | ----------------------------------------------- | ----------- |
| `::before`       | Generated content before the element.           | (0,0,0,1)   |
| `::after`        | Generated content after the element.            | (0,0,0,1)   |
| `::first-line`   | First rendered line of a block.                 | (0,0,0,1)   |
| `::first-letter` | First letter of the first line.                 | (0,0,0,1)   |
| `::marker`       | List bullet or number.                          | (0,0,0,1)   |
| `::placeholder`  | Form control placeholder.                       | (0,0,0,1)   |
| `::selection`    | Currently selected text.                        | (0,0,0,1)   |
| `::backdrop`     | Backdrop of a `<dialog>` or fullscreen element. | (0,0,0,1)   |

Pseudo-elements use double colons (`::`); the single-colon form (`:before`) is the CSS2 spelling and still works for the originals.

## Common gotchas

- ID selectors and `!important` make rules hard to override. Prefer classes and cascade layers.
- `:not(.a.b)` does not mean "not (a and b)" in older browsers; pass each as separate arguments: `:not(.a):not(.b)`.
- `:nth-child` ignores element types. `p:nth-child(2)` matches `p` only if it is also the 2nd child overall.
- `:has` is fully supported in evergreen browsers since 2023 but still missing in some embedded WebViews.
- `:is` and `:not` inherit the highest specificity of their arguments; `:is(.a, #b)` scores (0,1,0,0).
- Attribute selectors are case-sensitive by default. Use `[type="email" i]` for case-insensitive match.

## Related

- [[frontend/css]]
- [[frontend/css-has-selector]]
- [[frontend/css-cascade-layers]]
- [[cheatsheets/http-status-codes]]
- [[cheatsheets/index|Cheatsheets]]
