CSS Architecture
Decision Matrix
| Approach |
Best For |
Runtime Cost |
Type Safety |
| BEM |
Large teams, CMS themes, global CSS |
None |
No |
| Tailwind CSS |
Rapid prototyping, design systems |
None (purge) |
No |
| CSS Modules |
Component-scoped apps (React, Vue, Next.js) |
None |
No |
| styled-components / Emotion |
Dynamic theming, co-location |
Runtime |
Optional |
| Vanilla Extract |
Type-safe, zero-runtime, large apps |
None |
Yes |
| Panda CSS |
Utility-first with type safety |
None |
Yes |
Decision flow:
- Need type-safe styles? → Vanilla Extract or Panda CSS.
- Runtime perf critical? → Avoid styled-components/Emotion at scale.
- Need props-based dynamic styling? → CSS-in-JS or Panda CSS.
- Prefer utility classes? → Tailwind.
- Need global, sharable CSS? → BEM. Otherwise → CSS Modules.
Workflow
- Pick approach via matrix above.
- Define tokens as CSS custom properties on
:root (see snippet below).
- Declare cascade layers in order:
@layer reset, base, components, utilities, overrides;.
- Configure stylelint with the linter snippet below.
- Run validation commands at the bottom. If any check fails, fix before merging.
CSS Custom Properties (Tokens)
:root {
--color-primary: #2563eb;
--color-surface: #ffffff;
--color-text: #1a1a2e;
--space-sm: 0.5rem;
--space-md: 1rem;
--space-lg: 1.5rem;
--radius-md: 0.375rem;
}
[data-theme="dark"] {
--color-surface: #1a1a2e;
--color-text: #e2e8f0;
}
Cascade Layers
@layer reset, base, components, utilities, overrides;
@import url('vendor-library.css') layer(vendor);
@layer components {
.card { padding: var(--space-lg); background: var(--color-surface); }
}
@layer utilities {
.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0,0,0,0); }
}
Specificity Targets
| Selector |
Specificity |
Verdict |
.card |
0-1-0 |
Ideal |
.card .title |
0-2-0 |
Acceptable |
#main .card |
1-1-0 |
Avoid (ID) |
div.card |
0-1-1 |
Avoid (qualifier) |
If you reach for !important, fix the layer order or selector instead.
Stylelint Config (copy-paste)
.stylelintrc.json:
{
"extends": ["stylelint-config-standard"],
"rules": {
"selector-max-id": 0,
"selector-max-specificity": "0,3,0",
"selector-max-compound-selectors": 3,
"declaration-no-important": true,
"custom-property-pattern": "^[a-z][a-z0-9]*(-[a-z0-9]+)*$",
"no-descending-specificity": true
}
}
Validation Checklist
Run these before merging. Each must pass.
# 1. Lint passes with zero errors
npx stylelint "**/*.css"
# 2. Specificity audit: report any selector above 0,3,0
npx specificity-graph src/**/*.css
# 3. Search for !important (should return zero hits outside utilities layer)
grep -rn "!important" src/ --include="*.css" | grep -v "@layer utilities"
# 4. Search for ID selectors in stylesheets
grep -rnE "^#[a-zA-Z]" src/ --include="*.css"
# 5. Production CSS bundle size (target <50KB gzipped for most apps)
gzip -c dist/assets/*.css | wc -c
Browser test pass criteria:
- Theme toggle (
[data-theme="dark"]) updates all components without per-component overrides.
- Vendor library styles do not override your component styles (verify in DevTools Computed panel).
- Lighthouse "Unused CSS" report shows <10KB unused.
Deep Dive References
- BEM Guide — Naming, file organization, preprocessor integration
- Tailwind Patterns — Config, CVA, JIT, design tokens
- CSS Modules Guide — Scoping, composes, framework integration
- CSS-in-JS Patterns — Runtime vs build-time, Vanilla Extract, Panda
Next Steps
1---2name: css-architecture3description: Use when starting a new frontend project and choosing a CSS approach, refactoring inconsistent CSS, hitting specificity wars, integrating a component library, adding theming/dark mode, or deciding between BEM, Tailwind, CSS Modules, or CSS-in-JS.4---56# CSS Architecture78## Decision Matrix910| Approach | Best For | Runtime Cost | Type Safety |11|----------|----------|-------------|-------------|12| BEM | Large teams, CMS themes, global CSS | None | No |13| Tailwind CSS | Rapid prototyping, design systems | None (purge) | No |14| CSS Modules | Component-scoped apps (React, Vue, Next.js) | None | No |15| styled-components / Emotion | Dynamic theming, co-location | Runtime | Optional |16| Vanilla Extract | Type-safe, zero-runtime, large apps | None | Yes |17| Panda CSS | Utility-first with type safety | None | Yes |1819**Decision flow:**201. Need type-safe styles? → Vanilla Extract or Panda CSS.212. Runtime perf critical? → Avoid styled-components/Emotion at scale.223. Need props-based dynamic styling? → CSS-in-JS or Panda CSS.234. Prefer utility classes? → Tailwind.245. Need global, sharable CSS? → BEM. Otherwise → CSS Modules.2526## Workflow27281. Pick approach via matrix above.292. Define tokens as CSS custom properties on `:root` (see snippet below).303. Declare cascade layers in order: `@layer reset, base, components, utilities, overrides;`.314. Configure stylelint with the linter snippet below.325. Run validation commands at the bottom. If any check fails, fix before merging.3334## CSS Custom Properties (Tokens)3536```css37:root {38 --color-primary: #2563eb;39 --color-surface: #ffffff;40 --color-text: #1a1a2e;41 --space-sm: 0.5rem;42 --space-md: 1rem;43 --space-lg: 1.5rem;44 --radius-md: 0.375rem;45}4647[data-theme="dark"] {48 --color-surface: #1a1a2e;49 --color-text: #e2e8f0;50}51```5253## Cascade Layers5455```css56@layer reset, base, components, utilities, overrides;5758@import url('vendor-library.css') layer(vendor);5960@layer components {61 .card { padding: var(--space-lg); background: var(--color-surface); }62}6364@layer utilities {65 .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0,0,0,0); }66}67```6869## Specificity Targets7071| Selector | Specificity | Verdict |72|----------|------------|---------|73| `.card` | 0-1-0 | Ideal |74| `.card .title` | 0-2-0 | Acceptable |75| `#main .card` | 1-1-0 | Avoid (ID) |76| `div.card` | 0-1-1 | Avoid (qualifier) |7778If you reach for `!important`, fix the layer order or selector instead.7980## Stylelint Config (copy-paste)8182`.stylelintrc.json`:8384```json85{86 "extends": ["stylelint-config-standard"],87 "rules": {88 "selector-max-id": 0,89 "selector-max-specificity": "0,3,0",90 "selector-max-compound-selectors": 3,91 "declaration-no-important": true,92 "custom-property-pattern": "^[a-z][a-z0-9]*(-[a-z0-9]+)*$",93 "no-descending-specificity": true94 }95}96```9798## Validation Checklist99100Run these before merging. Each must pass.101102```bash103# 1. Lint passes with zero errors104npx stylelint "**/*.css"105106# 2. Specificity audit: report any selector above 0,3,0107npx specificity-graph src/**/*.css108109# 3. Search for !important (should return zero hits outside utilities layer)110grep -rn "!important" src/ --include="*.css" | grep -v "@layer utilities"111112# 4. Search for ID selectors in stylesheets113grep -rnE "^#[a-zA-Z]" src/ --include="*.css"114115# 5. Production CSS bundle size (target <50KB gzipped for most apps)116gzip -c dist/assets/*.css | wc -c117```118119**Browser test pass criteria:**120- Theme toggle (`[data-theme="dark"]`) updates all components without per-component overrides.121- Vendor library styles do not override your component styles (verify in DevTools Computed panel).122- Lighthouse "Unused CSS" report shows <10KB unused.123124## Deep Dive References125126- [BEM Guide](references/bem-guide.md) — Naming, file organization, preprocessor integration127- [Tailwind Patterns](references/tailwind-patterns.md) — Config, CVA, JIT, design tokens128- [CSS Modules Guide](references/css-modules-guide.md) — Scoping, composes, framework integration129- [CSS-in-JS Patterns](references/css-in-js-patterns.md) — Runtime vs build-time, Vanilla Extract, Panda130131## Next Steps132133- **[Design Tokens](../design-tokens/SKILL.md)**: Token layer feeding the architecture134- **[Frontend Components](../frontend-components/SKILL.md)**: Components consuming the styling135- **[Design System Creation](../design-system-creation/SKILL.md)**: Tie architecture to components136- **[Responsive Design](../responsive-design/SKILL.md)**: Responsive strategies within the chosen approach