| name | css-architecture |
| description | 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. |
| metadata | {"references":["references/bem-guide.md","references/css-in-js-patterns.md","references/css-modules-guide.md","references/tailwind-patterns.md"]} |
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.
npx stylelint "**/*.css"
npx specificity-graph src/**/*.css
grep -rn "!important" src/ --include="*.css" | grep -v "@layer utilities"
grep -rnE "^#[a-zA-Z]" src/ --include="*.css"
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
Next Steps