CSS Specificity & Architecture
Interview Question: "How does CSS specificity work? How do you architect CSS at scale?"
Specificity: The Scoring System
"Specificity is a four-part score: (inline, IDs, classes/attributes/pseudo-classes, elements/pseudo-elements). Higher score wins. In case of a tie, the last rule in source order wins."
| Selector | Specificity | Score |
|---|---|---|
* | (0,0,0,0) | 0 |
div | (0,0,0,1) | 1 element |
.card | (0,0,1,0) | 1 class |
#header | (0,1,0,0) | 1 ID |
style="" | (1,0,0,0) | Inline |
div.card.active | (0,0,2,1) | 2 classes + 1 element |
#nav .link:hover | (0,1,2,0) | 1 ID + 1 class + 1 pseudo-class |
The Rules
"Each category is independent â 256 classes cannot outweigh a single ID (in practice; technically it depends on the browser's implementation, but treat them as separate columns).
!importantoverrides everything except another!importantwith higher specificity. The cascade order: user-agent â user â author â author!importantâ user!important."
:where() vs :is()
/* :is() â takes the highest specificity of its arguments */
:is(.card, #hero) .title { color: red; } /* Specificity: (0,1,1,0) â ID wins */
/* :where() â always zero specificity */
:where(.card, #hero) .title { color: red; } /* Specificity: (0,0,1,0) â reset! */:where() is a game-changer for library CSS â it applies styles without increasing specificity, making overrides easy for consumers.
CSS Layers (@layer)
@layer base, components, utilities;
@layer base {
h1 { font-size: 2rem; }
}
@layer components {
.card h1 { font-size: 1.5rem; }
}
@layer utilities {
.text-xl { font-size: 1.25rem !important; }
}"Layers establish explicit specificity ordering regardless of selector complexity. Utilities layer always beats components, which always beats base. This is how Tailwind CSS v4 works under the hood."
CSS Architecture at Scale
The Problems
- Specificity wars â Every new feature needs more specific selectors
- Global scope â All CSS is global, names collide across teams
- Dead code â Unused CSS accumulates, can't safely delete
- Inconsistency â 47 shades of gray, 12 spacing values
Solutions Comparison
| Approach | Scope | Dead Code Detection | Trade-off |
|---|---|---|---|
| BEM | Convention-based | Hard | Verbose class names |
| CSS Modules | File-scoped (build tool) | Easy (per component) | Requires build step |
| Tailwind CSS | Utility classes | Automatic (purge) | HTML verbosity |
| CSS-in-JS | Component-scoped (runtime) | Automatic | Runtime cost, SSR complexity |
| CSS Layers | Explicit ordering | Manual | Browser support |
Modern Recommendation
"For most React apps: CSS Modules or Tailwind for component styling, with a design token system (CSS custom properties) for consistency. CSS-in-JS (styled-components, Emotion) has fallen out of favor due to runtime overhead and SSR complexity."
/* Design tokens via custom properties */
:root {
--color-primary: oklch(0.7 0.15 250);
--spacing-4: 1rem;
--radius-md: 0.5rem;
--shadow-sm: 0 1px 2px oklch(0 0 0 / 0.05);
}Red Flags
- Not knowing the specificity scoring system
- Using
!importantas a first resort instead of fixing specificity - Not knowing modern solutions (CSS Modules, Layers,
:where()) - No opinion on CSS architecture for large teams