CSS Story

CSS Story: Style Management

specificity, @layer, naming, tokens, !important

5.0 Overview

The cascade is powerful but becomes unpredictable as a project grows. Multiple stylesheets, third-party libraries, and component styles compete for the same elements, and the rule with the highest specificity wins regardless of where it appears in source order. The strategies in this chapter keep specificity predictable and source order explicit - four complementary tools: specificity discipline, cascade layers, naming conventions, and design tokens.

5.1 Specificity Discipline

The cascade computes a three-part score for every selector: (ID count, class/attribute/pseudo-class count, element/pseudo-element count). The selector with the higher score wins, regardless of source order. Conflicts become hard to reason about when selectors mix IDs, deep nesting, and element qualifiers, because the winning rule is no longer obvious. The practical rule: keep every styling selector at exactly one class - specificity (0,1,0). Deviating upward creates debt that has to be paid by more deviation later.
Table 1. - Selector specificity scores
Selector Score (I, C, E) Notes
* (0, 0, 0) Universal selector; no contribution.
p, div, ::before (0, 0, 1) Element and pseudo-element selectors.
.card, [type="text"], :hover (0, 1, 0) Class, attribute, and pseudo-class selectors.
#nav (1, 0, 0) ID selector; beats any number of classes.
:where(...) (0, 0, 0) Argument list is evaluated but contributes zero specificity.
:is(...), :not(...) Inherited Takes the specificity of the most specific selector in its argument list.
Inline style="" (1, 0, 0, 0) Above the three-part score; beaten only by !important.
Three patterns that keep specificity low:
/* instead of: nav > ul > li > a { color: steelblue; }   (0,0,3) */
.nav-link { color: steelblue; }                          /* (0,1,0) */

/* :where() - zero specificity, safe for resets and base styles */
:where(h1, h2, h3) { line-height: 1.2; }

/* :is() - grouping convenience; inherits specificity of its most-specific arg */
:is(.card, .panel) > p { margin-bottom: 0.75rem; }

5.2 Cascade Layers (@layer)

@layer assigns rules to named layers whose win order is declared once, independently of specificity. A rule in a later layer always beats a same-specificity rule in an earlier layer. Rules outside any layer sit above all layers and win unconditionally - useful for keeping third-party imports from overriding your styles.
/* declare order once at the top of the root stylesheet */
@layer base, theme, components, utilities;

@layer base {
  *, *::before, *::after { box-sizing: border-box; }
  body { margin: 0; font-family: sans-serif; }
}

@layer theme {
  :root {
    --color-text:   #1a1a2e;
    --color-accent: steelblue;
    --color-bg:     #ffffff;
  }
}

@layer components {
  .btn  { padding: 0.4rem 1rem; border-radius: 4px; }
  .card { padding: 1rem; border: 1px solid var(--color-accent); }
}

@layer utilities {
  .hidden  { display: none; }
  .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; }
}
Importing a third-party stylesheet into a layer contains it below your own unlayered rules:
/* bootstrap stays inside "vendor" - your unlayered rules win */
@layer vendor {
  @import url("bootstrap.min.css");
}
Within a layer, normal specificity and source order still apply. Across layers, layer order decides everything.

5.3 Naming Conventions

A consistent naming convention enforces uniform specificity and makes component boundaries visible in markup. BEM (Block-Element-Modifier) is the most widely adopted scheme:
  • .block - a standalone component (.card, .nav)
  • .block__element - a child part of that component (.card__title)
  • .block--modifier - a variant or state (.card--featured, .btn--danger)
/* every selector is a single class: specificity (0,1,0) throughout */
.card           { background: var(--color-bg); border-radius: 4px; padding: 1rem; }
.card__title    { font-size: 1.1rem; font-weight: bold; margin-bottom: 0.5rem; }
.card__body     { color: var(--color-text); }
.card--featured { border: 2px solid var(--color-accent); }
<div class="card card--featured">
  <h2 class="card__title">Title</h2>
  <p  class="card__body">Content</p>
</div>
Because every selector is one class, there are no specificity wars. A modifier class is added alongside the block class rather than overriding it, so both sets of rules apply cleanly.

5.4 Design Tokens with Custom Properties

Custom properties separate design decisions (color, spacing, radius) from layout rules. Define a named palette at :root; switch an entire theme by overriding a handful of properties in one place. Components reference tokens, never raw values, so a single change propagates everywhere.
/* token definitions */
:root {
  --color-text:       #1a1a2e;
  --color-bg:         #ffffff;
  --color-accent:     steelblue;
  --color-accent-dim: #b0c8e0;
  --space-sm:  0.5rem;
  --space-md:  1.0rem;
  --space-lg:  2.0rem;
  --radius:    4px;
  --shadow:    0 2px 8px rgb(0 0 0 / 12%);
}

/* dark theme - just override the tokens */
.theme-dark {
  --color-text: #e8e8f0;
  --color-bg:   #1a1a2e;
  --color-accent: #7eb8d8;
}
/* component uses only tokens, never raw values */
.card {
  background:    var(--color-bg);
  color:         var(--color-text);
  border-radius: var(--radius);
  padding:       var(--space-md);
  box-shadow:    var(--shadow);
}
Custom properties are inherited like any other CSS property. A token set on a container applies to all descendants, making scoped overrides straightforward:
/* invert accent inside a hero section without touching component rules */
.hero { --color-accent: #fce8c8; }

5.5 Controlling !important

!important overrides all normal specificity, including inline styles. That power makes it destructive when used to patch a specificity conflict - the next developer adds another !important and the arms race begins, eventually making styles impossible to reason about. Three legitimate uses:
  • Utility classes that must never be overridden by component styles:
.hidden  { display: none !important; }
.visible { display: block !important; }
  • Accessibility overrides - forced contrast, reduced motion:
@media (forced-colors: active) {
  .btn { border: 2px solid ButtonText !important; }
}
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: 0.01ms !important; }
}
  • Temporary debugging - always removed before committing.
When you find yourself reaching for !important to fix a specificity conflict, the right fix is almost always to reduce the specificity of the conflicting rule, restructure the selector, or use @layer to establish explicit win order.

5.6 Strategy Summary

Table 2. - Style management strategies
Strategy What it controls Key rule
Specificity discipline Which rule wins when two selectors match the same element Keep all styling selectors at one class (0,1,0). Use :where() for zero-cost grouping.
@layer Source-order priority across stylesheets and libraries Later layers beat earlier layers regardless of specificity. Unlayered rules beat all layers.
Naming (BEM) Enforces single-class selectors; encodes component structure .block__element--modifier - every selector is one class, self-documenting.
Design tokens Centralizes design values; enables theming without touching component rules Define at :root, override in a scoped context. Components reference only tokens.
!important Last-resort override; legitimate only for utilities and accessibility Never use to fix a specificity conflict - fix the selector instead.