A CSS selector is a pattern that identifies which elements in a document a style rule applies to. In CSS, every rule consists of a selector followed by a declaration block, and the browser matches the selector against the DOM to decide which elements receive those declarations. Selectors range from a single element name such as p to complex expressions that combine classes, attributes, states and structural relationships.
Selectors are defined in the W3C Selectors specification, now at Level 4, and the same syntax is reused outside stylesheets, for example in querySelectorAll() in JavaScript and in testing tools. Choosing selectors well affects maintainability, specificity conflicts and, in extreme cases, rendering performance.
How browsers match CSS selectors
A browser evaluates a selector by checking candidate elements against its conditions, and for complex selectors it typically works from right to left. In nav ul li a, the rightmost part, called the subject or key selector, is tested first: only elements that are links are considered, and the engine then walks up the ancestors to confirm the remaining conditions. This strategy discards most elements quickly.
A selector is built from simple selectors, such as a type, class, ID, attribute or pseudo-class, which are joined without spaces into a compound selector like button.primary:hover. Compound selectors are then linked by combinators into complex selectors, and several complex selectors can share one rule as a comma-separated selector list.
| Selector type | Syntax example | What it matches |
|---|---|---|
| Universal | * | Every element |
| Type | article | All elements with that tag name |
| Class | .card | Elements whose class list contains card |
| ID | #main | The element with that id |
| Attribute | [type="email"] | Elements with a matching attribute value |
| Pseudo-class | :focus-visible | Elements in a particular state or position |
| Pseudo-element | ::before | A generated part of an element, not a DOM node |
Combinators and structural relationships
Combinators express how elements relate in the document tree. They let one rule target, for example, only the links inside navigation or only the paragraph directly after a heading, without adding extra classes to the markup.
| Combinator | Syntax | Matches | Example meaning |
|---|---|---|---|
| Descendant | A B (space) | B anywhere inside A | Links anywhere in a footer |
| Child | A > B | B that is a direct child of A | Top-level list items only |
| Next sibling | A + B | B immediately after A | First paragraph after a heading |
| Subsequent sibling | A ~ B | Any B after A with the same parent | All paragraphs after a heading |
The relational pseudo-class :has() adds the long-missing ability to select an element based on its descendants or following siblings. A rule such as .card:has(img) styles only cards that contain an image, and label:has(+ input:invalid) styles a label whose field is invalid. It is supported in all major current browsers, but complex :has() chains should be used with care on very large documents.
Specificity and the cascade
When several rules set the same property on one element, the cascade decides the winner, and specificity is one of its central criteria. Specificity is calculated as a three-part value: IDs, then classes, attributes and pseudo-classes, then types and pseudo-elements. Values are compared column by column, so a single ID outranks any number of classes.
| Selector | IDs | Classes, attributes, pseudo-classes | Types, pseudo-elements |
|---|---|---|---|
p | 0 | 0 | 1 |
.note p | 0 | 1 | 1 |
a:hover | 0 | 1 | 1 |
#nav .item | 1 | 1 | 0 |
:where(.btn) | 0 | 0 | 0 |
:is(#nav, .menu) a | 1 | 0 | 1 |
Two functional pseudo-classes control specificity deliberately. :is() takes the specificity of its most specific argument, while :where() always contributes zero specificity, which makes it ideal for base styles in libraries that users should be able to override easily. Cascade layers declared with @layer add another level of control, because layer order is evaluated before specificity.
Choosing selectors in practice
Good selector strategy keeps specificity low and predictable. Most maintainable codebases style components primarily through single class selectors, use type selectors for base typography and resets, and reserve IDs for anchors and scripting rather than styling.
| Situation | Recommended approach | Reason |
|---|---|---|
| Reusable component | Single class such as .card | Low, flat specificity that is easy to override |
| Component variant | Modifier class or data attribute | Clear intent without nesting |
| Interactive state | :hover, :focus-visible, :disabled | Follows real state without script |
| Form validation styling | :user-invalid or :invalid | Reflects native constraint validation |
| Library defaults | :where() wrappers | Users can override without fighting specificity |
| Parent depends on content | :has() | Avoids extra classes added by script |
Methodologies such as BEM and utility-first frameworks like Tailwind CSS largely exist to avoid deep descendant selectors. They trade some verbosity in the markup for selectors that remain flat, which reduces unexpected overrides as projects grow.
Pitfalls and misconceptions
Most selector problems are architectural rather than syntactic. Long, highly specific chains work at first but couple styles tightly to the markup structure, so a small HTML change breaks them.
- Over-qualified selectors such as
div.header ul.menu li araise specificity and break when the structure changes. - Styling with IDs creates rules that classes cannot override without escalation.
- Using the important flag on declarations to win specificity battles, which only moves the conflict to a new level.
- Invalid selectors in a list: outside forgiving contexts such as
:is(), one unsupported selector invalidates the whole comma-separated rule. - Worrying about micro-performance: in typical pages selector speed rarely matters compared with layout and script costs.
The practical guideline is to write selectors that describe the component, not the page structure. Such selectors remain readable, survive refactoring and keep the cascade understandable for every developer on the team.
The simple explanation
A CSS selector is the part of a style rule that says what to style. It points at things on a web page, such as all headings, every button with a certain name or only the link the mouse is over.
It works like an address on an envelope. The address can be broad, such as everyone on this street, or very precise, such as the second flat on the third floor. The style rule is then delivered to whatever matches. The key idea is that simple, clear addresses are easier to manage than long, complicated ones.
How it is used in practice
A front-end developer joins a project where button styles keep breaking. The stylesheet contains rules such as #sidebar div.widget ul li a.button, and every new page needs a longer selector or an important flag to change a color. The developer measures the problem: some selectors reach a specificity of one ID and four classes.
The team refactors the buttons into a component styled by a single .btn class with modifier classes for variants and :focus-visible for keyboard focus. Base styles from the design system are wrapped in :where(), giving them zero specificity, and cascade layers separate resets, components and utilities. After the change, a new button variant needs one short rule, the important flags disappear and visual regressions from markup changes stop appearing in code review.