CSS selector

On this page 7

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 typeSyntax exampleWhat it matches
Universal*Every element
TypearticleAll elements with that tag name
Class.cardElements whose class list contains card
ID#mainThe element with that id
Attribute[type="email"]Elements with a matching attribute value
Pseudo-class:focus-visibleElements in a particular state or position
Pseudo-element::beforeA generated part of an element, not a DOM node
Basic selector types

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.

CombinatorSyntaxMatchesExample meaning
DescendantA B (space)B anywhere inside ALinks anywhere in a footer
ChildA > BB that is a direct child of ATop-level list items only
Next siblingA + BB immediately after AFirst paragraph after a heading
Subsequent siblingA ~ BAny B after A with the same parentAll paragraphs after a heading
The four main combinators

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.

SelectorIDsClasses, attributes, pseudo-classesTypes, pseudo-elements
p001
.note p011
a:hover011
#nav .item110
:where(.btn)000
:is(#nav, .menu) a101
Specificity of example selectors

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.

SituationRecommended approachReason
Reusable componentSingle class such as .cardLow, flat specificity that is easy to override
Component variantModifier class or data attributeClear intent without nesting
Interactive state:hover, :focus-visible, :disabledFollows real state without script
Form validation styling:user-invalid or :invalidReflects native constraint validation
Library defaults:where() wrappersUsers can override without fighting specificity
Parent depends on content:has()Avoids extra classes added by script
Selector choices for common situations

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 a raise 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.