CSS in React
css in react css in react spans styled-components, emotion, stylex, vanilla-extract, linaria, and tailwind-in-jsx — staff engineers evaluate based on
Introduction
CSS in React spans styled-components, Emotion, StyleX, vanilla-extract, Linaria, and Tailwind-in-JSX — staff engineers evaluate based on runtime cost, SSR extraction, token integration, and whether styles are co-located with component logic or generated at build time.
The React team's rendering model (concurrent features, RSC, streaming) changed which CSS-in-JS libraries remain viable for App Router — runtime injection libraries face explicit trade-offs.
Business problem
Business pressure: Product wants themeable components with prop-driven variants — size="lg" intent="danger". Co-locating styles with React components speeds feature delivery but wrong library choice adds 30KB runtime and breaks SSR streaming.
- DX: Styles beside JSX — fewer context switches.
- Dynamic UI: Prop-driven variants without maintaining BEM modifier classes manually.
- RSC boundary: Server Components cannot use runtime CSS-in-JS — architectural fork.
Why this feature exists
Platform history: styled-components (2016) popularized tagged template literals in JS. Emotion followed with performance focus. Meta's StyleX and vanilla-extract push compile-time extraction for scale.
- Problem solved: Global CSS + React component model mismatch.
- Rejected alternative: CSS Modules only — verbose for dynamic variants.
- Modern role: Hybrid — RSC + CSS Modules/ Tailwind + client islands with runtime CSS where needed.
Browser rendering perspective
Runtime CSS-in-JS: Injects <style> tags on render — blocks or interleaves with React hydration. Compile-time: static CSS files — identical pipeline to Modules.
- INP risk: Style recalc on every variant mount if library injects per component instance.
- StyleX/Linaria: Build extracts atomic CSS — no runtime injection.
Internal browser workflow
Runtime path: React render → library serializes styles → insert style tag → class assigned. Build path: Babel/Vite plugin extracts CSS at compile → link in document head.
Feature deep dive
Categories: Runtime (styled-components, Emotion), Atomic compile-time (StyleX, Linaria), Zero-runtime (vanilla-extract, CSS Modules), Utility (Tailwind className).
// styled-components — runtimeconst Button = styled.button`padding: 0.5rem 1rem;background: ${props => props.$primary ? 'var(--primary)' : '#fff'};`;// vanilla-extract — build timeimport { style } from '@vanilla-extract/css';export const btn = style({ padding: '0.5rem 1rem' });
Syntax
Transient props: $prefix in styled-components avoids DOM attribute leak. css prop Emotion — use sparingly for one-offs.
Examples
ThemeProvider token bridge:
const theme = { colors: { primary: '#4f46e5' } };<ThemeProvider theme={theme}><Button $primary>Save</Button></ThemeProvider>
Real-world use
Meta uses StyleX on facebook.com surfaces. Discord migrated concerns toward Tailwind. Many Next.js apps dropped runtime styled-components for RSC compatibility — moved to Tailwind or CSS Modules.
Real production example
Pattern: RSC layout with CSS Modules; client components use vanilla-extract or Tailwind; ban runtime CSS-in-JS in server components via eslint.
Enterprise use case
Enterprise: ADR per app tier — marketing Tailwind, product app CSS Modules + tokens, legacy styled-components quarantined.
Accessibility considerations
A11y: Runtime libraries support :focus-visible in template — do not lose pseudo states. Server-rendered class mismatch breaks hydration focus styles.
Performance considerations
Performance: Prefer zero-runtime for high-traffic surfaces. Profile React Profiler + Performance panel for style injection during mount storms.
SEO considerations
SEO: SSR must extract styles to head for FCP — styled-components SSR APIs; or use static extraction.
Scalability considerations
Scale: ThemeProvider at root — changing theme re-renders subtree; use CSS variables + single class toggle on html for scale.
Common production issues
Failures: Hydration mismatch when class names differ server/client. RSC import of styled component crashes build.
Debugging guide
Debug: Count style tags in head during navigation — growth indicates per-mount injection leak.
Best practices
- Zero-runtime for App Router default.
- Theme via CSS variables on :root.
- eslint boundaries: no styled in server files.
Anti-patterns
- Global styled-components createStyle reset without isolation plan.
- css prop on every element in hot paths.
Trade-offs
- Runtime: dynamic props easy; perf + RSC cost.
- Compile-time: perf win; less dynamic unless variables.
Architecture review questions
- Which components still use runtime CSS-in-JS and why?
- RSC-compatible styling strategy documented?
Interview questions
Why did Next.js App Router complicate styled-components?(Advanced)
Server Components cannot run client-only style injection. Runtime libraries need client boundary and SSR extraction — streaming RSC prefers static CSS. Teams migrated to compile-time or utility CSS.
Follow-up: Compare StyleX vs vanilla-extract
Hands-on exercise
Exercise: Same button in styled-components vs vanilla-extract — measure client JS and style tag count.
Staff engineer notes
- CSS-in-React choice is a React rendering model decision — revisit after RSC adoption.
Common pitfalls
- Importing runtime styled in server component.
Try it yourself
Edit the CSS panel — the preview updates live. Use DevTools Performance and Accessibility panels to validate.
Try it yourself
Summary
CSS in React encompasses runtime and compile-time styling strategies — staff engineers align library choice with RSC boundaries, SSR extraction, and runtime performance budgets.
Key takeaways
- CSS-in-React spans runtime and compile-time approaches.
- RSC pushes zero-runtime and static extraction.