Lesson · Beginner
CSS selectors and specificity
Select elements by stable roles and resolve cascade conflicts without escalating to brittle selectors or unnecessary important declarations.
- Published
- Last checked
What this solves
Selectors connect rules to elements. The cascade decides which declaration wins
when several rules set the same property. Understanding that decision prevents
the common cycle of adding IDs, deep nesting, and !important until the current
screen looks right but future changes become dangerous.
Minimal correct example
@layer reset, base, components, utilities;
@layer base {
a {
color: var(--link-color);
}
}
@layer components {
.button-link {
display: inline-flex;
padding: 0.75rem 1rem;
}
}
The element selector establishes a broad default. The class names a reusable role. Layers make the intended category order explicit without inflating specificity.
How browsers actually behave
The cascade compares relevance, origin and importance, layer order, specificity, scoping proximity where applicable, and source order. Specificity is compared in columns; it is not a single decimal score. A later declaration wins only after higher-priority factors tie.
Inheritance is separate. Properties such as color often inherit from a parent, while margin and border generally do not. DevTools shows the winning declaration, crossed-out competitors, inherited sources, and computed result. Use that evidence before editing.
Decisions and trade-offs
Use element selectors for honest document defaults and classes for reusable component roles or states. Attribute selectors are useful for real attributes, but broad substring matches can become expensive to reason about. Avoid tying styles to deep DOM structure when a component's internal wrapper can change.
Keep specificity deliberately low. :where() contributes no specificity and can
wrap optional context. Cascade layers can order vendor, reset, base, component,
and utility rules. Reserve !important for carefully bounded cases such as some
user-preference utilities, not routine conflict resolution.
Accessibility consequences
Selector conflicts often break focus, error, disabled, selected, reduced-motion, or forced-color states because a visually stronger rule overrides them. State styles need persistent cues and enough cascade priority to work across hover, dark mode, and responsive variants.
Do not select controls by visual position such as “the third button.” The DOM can change with validation or translation. Name stable roles and test states using the same semantics the component exposes.
Common failures
- A rule is crossed out. Another declaration wins earlier in the cascade. Inspect origin, layer, importance, specificity, and order before changing it.
- A selector breaks after adding a wrapper. It depends on deep structure. Replace it with a role-based class.
- Focus works until hover applies. A later state rule overrides the ring. Review combined states and preserve a visible indicator.
- One exception needs several important declarations. The stylesheet lacks explicit layers or component boundaries. Repair the architecture instead of adding more weight.
- Styles leak into unrelated content. A selector is too broad. Scope it to the actual component role without matching incidental descendants.
Verify it
Use DevTools to identify the winning and losing declaration for every conflict. State why it won before editing. Search the stylesheet for IDs, important declarations, deep descendant chains, and positional selectors. Exercise normal, hover, focus-visible, active, disabled, invalid, selected, reduced-motion, dark, and forced-color states at 320 pixels and 200% zoom. Confirm a markup wrapper change does not silently alter the public component contract.
Exercise and next step
Create a base link style, a button-like link class, and a navigation-current
state using low-specificity selectors. Introduce one deliberate conflict,
diagnose it in DevTools, and fix the architecture without an ID or !important.
Then apply the system to the responsive cards from the box-model exercise.
Practice with a real conflict
Copy a small component into two contexts, add a validation state, and apply a dark color preference. Introduce one vendor rule and one utility rule in named layers. Predict the winning declaration for color, border, and outline before opening DevTools, then compare the result. Refactor until the component works without DOM-position selectors or important declarations. Document the component class and state attributes so later markup changes preserve its styling contract.
Repeat the comparison after moving the component later in the stylesheet and inside a different parent. If its core presentation changes for reasons unrelated to an explicit variant, the selector or layer contract remains too dependent on incidental source order. Repair that dependency and record the intended extension point for future variants.
Run the same component with its focus, disabled, invalid, and high-contrast states visible together. DevTools should explain each winning declaration without an escalating chain of overrides. If fixing one state breaks another, reduce the selector contract or separate the variant explicitly before adding more weight.
Free launch-readiness starter
Take a reviewed page template and pre-publish checklist
Start with one semantic HTML page, then use the checklist on the real deployed response. The download is useful on its own.