Frontend CSS Strategy
Purpose
Choose and implement the right CSS approach for the project. Utility-first for rapid iteration and consistency. CSS Modules for scoped, component-encapsulated styles. CSS-in-JS for dynamic theming and colocation. Each approach solves a different problem — pick the right one.
Agent Protocol
Trigger
Exact phrases: "CSS strategy", "CSS Modules", "CSS-in-JS", "utility-first", "Tailwind CSS", "styled-components", "Emotion", "CSS organization", "CSS architecture", "CSS approach", "styling approach", "how to style", "CSS decision".
Input Context
- Framework (React, Vue, Angular, Svelte)
- Team size and CSS experience
- Project type (library, app, design system)
- Current styling approach (if any)
- Performance requirements (runtime cost, bundle size)
Output Artifact
CSS strategy recommendation with rationale, setup code, and organization pattern.
Response Format
## Recommendation
<approach> — <rationale>
## Setup
<config, dependencies, file-structure>
## Usage
<component-example, naming-convention>
## Organization
<folder-structure, globals, utilities>
—
Compression footer: frontend-css/v1 | approach: <tailwind|modules|css-in-js> | perf: <runtime|zero>
No preamble. No postamble. No explanations. No filler/hedging/transitions. Compress output — why use many token when few do trick.
Completion Criteria
- CSS approach selected with documented rationale
- File and folder structure created
- Global styles (reset, variables, typography) configured
- Component styling pattern established with examples
- Responsive and state-based styling pattern defined
- Build tool configured for chosen approach
Max Response Length
4096 tokens
Workflow
1. Approach Decision
Project type?
├── Design system / component library -> CSS Modules or vanilla-extract (zero runtime, scoped)
├── Large app with many developers -> Tailwind CSS (consistent, low decision fatigue)
├── Highly themed / white-label app -> CSS-in-JS or CSS variables (dynamic theming)
├── Micro-frontend -> CSS Modules (isolation)
└── Small app / prototype -> Any — pick based on team preference
2. Approach Comparison
| Approach | Runtime | Scoping | Dynamic Themes | Bundle Size | Learning Curve |
|---|---|---|---|---|---|
| Utility-first (Tailwind) | Zero | N/A (utility classes) | Via CSS vars | Small (purged) | Low-Medium |
| CSS Modules | Zero | Automatic (hash) | Via CSS vars | Zero runtime | Low |
| styled-components | Runtime | Automatic (hash) | Native | ~12KB runtime | Medium |
| Emotion | Runtime | Automatic (hash) | Native | ~8KB runtime | Medium |
| vanilla-extract | Zero | Automatic (hash) | Via CSS vars | Zero runtime | Medium |
| Linaria | Zero | Automatic (hash) | Via CSS vars | Zero runtime | Medium |
| Stitches | Runtime | Automatic | Native | ~5KB runtime | Medium |
| Raw CSS/Sass | Zero | Manual (BEM) | Via CSS vars | Zero | Low |
3. File Organization
src/
styles/
reset.css /* CSS reset */
variables.css /* CSS custom properties */
typography.css /* Font faces, text styles */
utilities.css /* Utility classes (if not Tailwind) */
animations.css /* Keyframes */
components/
Button/
Button.tsx
Button.module.css
Button.test.tsx
Card/
Card.tsx
Card.module.css
Card.test.tsx
pages/
Home/
Home.tsx
Home.module.css
4. CSS Modules Pattern
/* Button.module.css */
.root {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
border-radius: 6px;
font-weight: 500;
}
.primary {
background: var(--color-primary);
color: white;
}
.secondary {
background: transparent;
border: 1px solid var(--color-border);
}
5. Tailwind Pattern
export function Button({ variant = 'primary', ...props }: ButtonProps) {
const variants = {
primary: 'bg-blue-600 text-white hover:bg-blue-700',
secondary: 'bg-transparent border border-gray-300 hover:bg-gray-50',
}
return (
<button
className={`inline-flex items-center gap-2 px-4 py-2 rounded-md font-medium transition-colors ${variants[variant]}`}
{...props}
/>
)
}
6. Styled Components Pattern
import styled, { css } from 'styled-components'
interface ButtonProps {
$variant: 'primary' | 'secondary'
$large?: boolean
}
export const StyledButton = styled.button<ButtonProps>`
display: inline-flex;
align-items: center;
gap: 8px;
padding: ${({ $large }) => ($large ? '12px 24px' : '8px 16px')};
border-radius: 6px;
font-weight: 500;
cursor: pointer;
${({ $variant }) =>
$variant === 'primary'
? css`
background: var(--color-primary);
color: white;
`
: css`
background: transparent;
border: 1px solid var(--color-border);
`}
`
7. Vanilla Extract Pattern
// Button.css.ts
import { style, recipe } from '@vanilla-extract/css'
import { vars } from './theme.css'
export const button = recipe({
base: {
display: 'inline-flex',
alignItems: 'center',
gap: '8px',
padding: '8px 16px',
borderRadius: '6px',
fontWeight: 500,
},
variants: {
variant: {
primary: { background: vars.color.primary, color: 'white' },
secondary: { background: 'transparent', border: vars.color.border },
},
},
})
8. CSS Variables for Theming
:root {
--color-primary: #2563eb;
--color-primary-hover: #1d4ed8;
--color-background: #ffffff;
--color-text: #1a1a1a;
--color-border: #e5e7eb;
--radius-sm: 4px;
--radius-md: 8px;
--spacing-1: 4px;
--spacing-2: 8px;
--spacing-4: 16px;
}
9. PostCSS Configuration
// postcss.config.js
module.exports = {
plugins: [
require('postcss-import'),
require('postcss-nesting'), // or postcss-nested
require('autoprefixer'),
require('cssnano')({ preset: 'default' }),
],
}
10. Container Queries with CSS Strategy
/* Component-scoped responsive design */
.card-container {
container-type: inline-size;
}
@container (min-width: 400px) {
.card {
display: grid;
grid-template-columns: 200px 1fr;
}
}
Component Architecture
Decision Tree for Mixed Approaches
Page layout (grid, sections)
-> Tailwind utility classes or CSS Grid in global styles
-> Reason: layout changes infrequently, benefits from standard grid
Component appearance (color, spacing, typography)
-> CSS Modules or styled-components
-> Reason: encapsulation, no class name collisions
Dynamic styles (theme-dependent, user-customizable)
-> CSS Variables
-> Reason: runtime theme switching without re-render
Animations
-> CSS keyframes in global animations.css
-> Reason: reusable, hardware-accelerated
CSS Layers Strategy (cascade layers)
/* Define layer order — lower priority first */
@layer reset, base, tokens, components, utilities, overrides;
/* Reset — lowest priority */
@layer reset {
*, *::before, *::after { box-sizing: border-box; margin: 0; }
}
/* Base — element defaults */
@layer base {
body { font-family: system-ui; line-height: 1.5; }
}
/* Components — scoped component styles */
@layer components {
.card { border-radius: 8px; padding: 16px; }
}
/* Utilities — highest priority (win over components) */
@layer utilities {
.mt-4 { margin-top: 16px; }
}
CSS Layers solve specificity wars by letting you define priority order explicitly. Tailwind v4 uses layers internally.
Common Pitfalls
- Mixing approaches inconsistently: Using Tailwind in some components and CSS Modules in others without clear boundaries.
- Runtime CSS-in-JS for static apps: Adds unnecessary JS bundle when CSS Variables would work.
- Over-nesting in SCSS: More than 3 levels deep creates specificity problems.
- Not purging unused styles: With utility frameworks, purging is essential to keep bundle small.
- Inline styles for dynamic values: Use CSS variables instead (avoids specificity, enables transitions).
- Missing design tokens: Hardcoding values leads to inconsistency.
- Specificity wars:
!importantcascading indicates architectural problem. - CSS-in-JS during SSR: Some libraries (styled-components) require babel plugin for SSR. Always verify SSR compatibility.
- Global CSS leakage: CSS Modules and Shadow DOM prevent this. BEM and utility classes don't guarantee it.
- Font loading flash: Always specify
font-display: swaporfont-display: optionalfor web fonts.
Best Practices
- Pick one primary approach and stick with it — avoid mixing without clear boundaries.
- Design tokens in CSS custom properties, not JavaScript.
- Component styles never depend on global styles for layout.
- Utility classes preferred over one-off CSS for spacing, type, layout.
- CSS-in-JS only when dynamic theming is required beyond CSS variables.
- CSS Modules for zero-runtime scoping when dynamic theming not needed.
- Naming conventions consistent: camelCase for CSS Modules, kebab-case for utilities.
- Media queries use standard breakpoint values.
- Prefer CSS animations over JavaScript.
- Configure purging (Tailwind) or lint rules for dead styles.
Compared With
| Aspect | Tailwind | CSS Modules | styled-components |
|---|---|---|---|
| Setup time | 5 min | 1 min | 5 min |
| Bundle impact | 0KB after purge | 0KB | ~12KB runtime |
| Design consistency | Excellent (constraint-based) | Manual | Manual |
| Learning curve | Medium | Low | Low |
| Dynamic theming | Via CSS vars | Via CSS vars | Native |
| Dev experience | Excellent with IDE plugin | Standard | Good with babel plugin |
| Migration difficulty | High to change | Low | Medium |
Performance
- Tailwind purged: final CSS is typically 5-15KB gzipped for a large app.
- styled-components/Emotion: ~8-12KB runtime + inlined styles in JS bundle.
- CSS Modules: zero runtime cost, styles extracted to static CSS files.
- CSS Variables: no performance overhead, native browser optimization.
- Runtime CSS-in-JS adds ~0.4ms per style injection on initial render.
- Critical CSS extraction (inlining above-fold styles) improves FCP by 10-20%.
- CSS Layers have no performance overhead — they're a cascade-ordering feature only.
- Container Queries have same performance as media queries — negligible cost.
Browser Rendering Considerations
- CSS-in-JS during hydration can cause "flash of unstyled content" (FOUC) if SSR is not configured.
- CSS Variables are resolved at computed-value time — referencing many vars in one rule is slightly slower than literals but negligible in practice.
- Container Queries require the browser to track container dimensions — this has minimal overhead (similar to ResizeObserver).
@layerhas no performance cost — it is purely a cascade-ordering mechanism.
Tooling
tailwindcss— utility-first CSS framework with JIT compiler.postcss— CSS transformer (autoprefixer, nesting, custom media).sass— SCSS preprocessor with mixins, functions, variables.stylelint— CSS linter with rules for ordering, naming, specificity.vanilla-extract— zero-runtime CSS-in-JS with TypeScript.linaria— zero-runtime CSS-in-JS with Babel/Macro.critters— inline critical CSS for SSR frameworks.purgecss— remove unused CSS (used by Tailwind internally).lightningcss— Rust-based CSS parser/minifier (used by Vite, Parcel).cssnano— PostCSS-based CSS minifier.
Rules
- CSS approaches are not mixed in a single project — pick one primary approach.
- Design tokens live in CSS custom properties, not in JavaScript.
- Component styles never depend on global styles for layout — each component is self-contained.
- Utility classes are preferred over one-off CSS for margins, padding, typography, and layout.
- CSS-in-JS is only chosen when dynamic theming is required and CSS variables are insufficient.
- CSS Modules are used for zero-runtime scoping when dynamic theming is not needed.
- Naming conventions are consistent across the entire codebase.
- Media queries inside component styles use the project's standard breakpoint values.
- Animations prefer CSS over JavaScript — only JS animation when complex choreography is needed.
- Dead styles are removed — purging or lint rules are configured.
!importantis never used unless overriding a third-party library.- CSS selectors never exceed 3 levels of specificity.
- PostCSS or LightningCSS is configured for autoprefixing and minification.
- CSS Layers are used to manage cascade order explicitly.
References
- references/container-queries.md — Container Queries
- references/css-approaches.md — CSS Approaches
- references/css-custom-properties.md — CSS Custom Properties
- references/css-methodology.md — CSS Methodology
- references/css-organization.md — CSS Organization
- references/css-performance.md — CSS Performance
- references/css-architecture-methodologies.md — CSS Architecture Methodologies
- references/css-performance-bundle-optimization.md — CSS Performance Optimization
Handoff
No artifact produced unless requested.
Next skill: frontend-tailwind-css — Tailwind-specific patterns, configuration, and optimization.
Carry forward: CSS approach selected, organization pattern, theming via CSS variables.
Implementation Patterns
Observer Pattern for Event Handling
` interface EventObserver { onEvent(event: T): Promise; }
class EventBus { private observers: Set<EventObserver> = new Set(); subscribe(observer: EventObserver): void { this.observers.add(observer); } unsubscribe(observer: EventObserver): void { this.observers.delete(observer); } async emit(event: T): Promise { const results = Array.from(this.observers).map(o => o.onEvent(event)); await Promise.allSettled(results); } } `
Configuration-Driven Approach
config: defaults: timeout: 30s retryCount: 3 overrides: production: timeout: 60s retryCount: 5 development: timeout: 300s retryCount: 1
Production Considerations
Deployment Checklist
- Configuration validated against schema before startup
- Health check endpoints registered and monitored
- Graceful shutdown with draining period (30s timeout)
- Resource limits configured (CPU, memory, file descriptors)
- Log level set appropriate for environment
- Metrics endpoint secured and exposed
- Rate limiting configured per-tier
- TLS certificates valid and auto-renewing
- Database migrations run as separate deployment step
- Feature flags ready for gradual rollout
Monitoring and Alerting
| Metric | Threshold | Severity | Action |
|---|---|---|---|
| Error rate | > 1% over 5min | Critical | Page on-call |
| p99 latency | > 2s over 5min | Warning | Investigate |
| Throughput drop | > 50% over 1min | Critical | Check upstream |
| Queue depth | > 1000 over 1min | Warning | Scale consumers |
| Disk usage | > 85% | Warning | Clean or expand |
| Memory usage | > 90% heap | Critical | Restart or scale |
Anti-Patterns
| Anti-Pattern | Symptom | Root Cause | Solution |
|---|---|---|---|
| Premature optimization | Complex code for no measured benefit | Guessing instead of profiling | Measure first, optimize based on data |
| Copy-paste reuse | Duplicate code across codebase | Lack of abstraction | Extract shared logic into libraries |
| Gold-plating | Features with no current requirement | Over-engineering | YAGNI — build what's needed now |
| Magical thinking | Assumptions without validation | Skipping error handling | Handle all failure modes explicitly |
Performance Optimization
Caching Strategy
Cache hierarchy: L1 (in-memory local) → L2 (distributed Redis/Memcached) → L3 (CDN/Edge). Cache invalidation: TTL-based (simple, stale), event-based (complex, fresh), write-through (consistent, higher write latency), write-behind (fast writes, eventual consistency).
Resource Pooling
- Database connections: Pool of reusable connections (HikariCP, pgBouncer)
- HTTP connections: Keep-alive + connection pooling for external calls
- Thread pool: Bounded thread pools for async task execution
Profiling Methodology
- Establish baseline with production traffic profile
- Profile CPU with sampling profiler (pprof, perf, async-profiler)
- Profile memory with heap dumps and allocation tracking
- Profile I/O with strace/perf trace for syscall analysis
- Profile latency with distributed tracing (OpenTelemetry)
- Identify bottleneck, formulate hypothesis, implement fix
- Re-profile to verify improvement, repeat
Security Considerations
Threat Modeling (STRIDE)
- Spoofing: Identity validation, authentication
- Tampering: Integrity checks, digital signatures
- Repudiation: Audit logs, non-repudiation
- Information disclosure: Encryption, access control
- Denial of service: Rate limiting, resource quotas
- Elevation of privilege: Principle of least privilege
Supply Chain Security
- Dependency scanning: Snyk, Dependabot, Trivy
- SBOM generation: CycloneDX or SPDX format
- Signed commits: GPG or SSH commit signing
- Artifact verification: Checksum validation, signature verification
Secrets Management
- Secrets never in code — always in secrets manager (Vault, AWS Secrets Manager)
- Rotation policy: Rotate database credentials every 90 days
- Access audit: Log every secrets access, alert on anomalies
- Encryption at rest and in transit for all secrets
- Principle of least privilege: each service gets only its own secrets
Architecture Decision Trees
CSS Approach Decision Tree
Is the project a design system or standalone app?
├── Design system → CSS Custom Properties + Shadow DOM or CSS Modules
└── Standalone app → Is SSR required?
├── Yes → CSS Modules or utility-first (Tailwind) with JIT
└── No → CSS-in-JS or utility-first (Tailwind)
Team prefers runtime or build-time?
├── Runtime → CSS-in-JS (emotion, styled-components)
└── Build-time → Tailwind or CSS Modules
Organization Strategy Decision Tree
How many developers work on CSS?
├── 1-3 → Loose conventions + BEM or Tailwind
├── 3-10 → Structured methodology (ITCSS, SMACSS) + lint rules
└── 10+ → Design token system + component-scoped styles + strict linting
Is the project multi-brand?
├── Yes → Design tokens with theme layers + CSS variables
└── No → Single design language with consistent tokens