Skip to main content
ADR-003accepted

styled-components over Tailwind CSS

Context

The portfolio requires a theming system that supports runtime switching between dark and light modes without page reload. The design language demands precise control over micro-interactions, transitions, and conditional styling based on component state. The site's visual identity is achromatic with a single accent color — a system that requires theme tokens to be deeply integrated into every styled element. Tailwind CSS offers utility-first rapid development but generates static class strings at build time. Runtime theme switching with Tailwind requires CSS custom properties or a PostCSS plugin chain, both of which add indirection. styled-components provides a ThemeProvider that injects theme tokens directly into component scope, enabling conditional styling as native JavaScript expressions.

Decision

Adopt styled-components with a ThemeProvider pattern as the sole styling solution. Theme objects define all design tokens (colors, borders, shadows) and are swapped at the provider level for dark/light mode. Each styled component accesses theme values via the theme prop, enabling expressions like `${({ theme }) => theme.colors.borderDefault}`. Co-located styles keep visual logic adjacent to component logic, eliminating the cognitive overhead of mapping utility classes to design intent. No global CSS framework is used; GlobalStyle handles only resets and base typography.

Consequences

Positive: Runtime theming works seamlessly — toggling dark/light mode triggers a re-render with the new theme object, and every component updates atomically. Co-located styles eliminate class name management and make component boundaries self-documenting. Conditional styling based on props (e.g., `$active` state in navigation) is expressed as native JavaScript, not className string concatenation. Negative: Runtime CSS-in-JS adds ~12KB to the JavaScript bundle (styled-components runtime). Server-side rendering requires additional configuration (ServerStyleSheet) to avoid FOUC. Build-time extraction is not the default, so critical CSS is not automatically inlined. The DX benefit of co-located, prop-driven styles justifies the bundle cost for a portfolio-scale application where the total JS payload is well under performance budgets.

Calibrated Uncertainty

Predictions at Decision Time

Expected runtime theming to be the critical differentiator — specifically, instant dark/light mode switching without flicker or reload. Predicted the ~12KB bundle cost would be negligible relative to the total JS payload. Assumed that co-located styles would improve long-term maintainability by keeping styling decisions adjacent to component logic. Predicted no significant performance impact from runtime CSS generation at portfolio scale.

Measured Outcomes

Runtime theming works exactly as designed — dark/light toggle is instantaneous with no FOUC (ServerStyleSheet configuration was the key). The 12KB bundle cost is indeed negligible (~8% of total JS). Maintainability prediction was accurate: when revisiting components after months, the co-located styles are immediately comprehensible. However, the styled-components ecosystem has shifted significantly since the decision — Tailwind has won the broader ecosystem war, styled-components v6 introduced breaking changes, and the React team's recommendation has moved toward zero-runtime solutions (CSS Modules, Panda CSS). The library is not abandoned but is clearly in maintenance mode.

Unknowns at Decision Time

Did not anticipate the industry-wide shift away from runtime CSS-in-JS. At decision time, styled-components was the dominant CSS-in-JS library with strong community momentum. The React team's stance on runtime CSS-in-JS (specifically, Seb Markbage's posts about the performance ceiling of runtime approaches in React Server Components) was not publicly known. Also unknown: whether Tailwind v4 would solve the runtime theming gap with native CSS custom property support — it largely did.

Reversibility Classification

Two-Way Door

Migration from styled-components to Tailwind or CSS Modules is a component-by-component refactor that can happen incrementally. Both systems can coexist in the same Next.js project during migration. Each component's styles are self-contained, so converting one component doesn't affect others. Estimated effort for full migration: 20-30 hours across 120+ components. No data model or API changes required.

Strongest Counter-Argument

Tailwind CSS with CSS custom properties for theming would have provided: zero runtime JavaScript cost, better tree-shaking, faster build times, and alignment with the industry's direction. Tailwind's utility-first approach also constrains design decisions in ways that enforce consistency — a benefit for a solo developer who is also the designer. The DX trade-off (utility classes vs template literals) is largely a matter of familiarity, not objective superiority. The counter-counter: at the time of the decision, Tailwind's theming story required significant custom configuration, and the portfolio's design system was already expressed as JavaScript theme objects.

Technical Context

Stack
styled-components 5.3React 18ThemeProviderServerStyleSheet
Bundle Cost
~12KB gzipped
Theme Tokens
24
Styled Components
120+
Constraints
  • Runtime theme switching required
  • No page reload on theme change
  • SSR FOUC prevention needed

Related Decisions