メインコンテンツにスキップ

Why I Chose Emotion for My CSS Architecture

· 17 分の読書
Evan Carter
Evan Carter
Senior frontend

TLDR:

Emotion CSS

Emotion CSS offers a flexible, high-performance foundation for scalable CSS-in-JS architecture. In this deep-dive, I explain why I chose Emotion over styled-components, how to use @emotion/react and @emotion/styled effectively, and how to approach zero-runtime CSS—while aligning styling decisions with Feature-Sliced Design for long-term maintainability.

Emotion CSS sits right at the center of the modern css-in-js debate: you want styling that scales with product complexity, but you don’t want your UI layer to become a tangled, runtime-heavy blob that slows teams down. In real-world React systems, the biggest failure mode is not “bad CSS,” it’s uncontrolled coupling between styling, component boundaries, and feature boundaries—exactly where Feature-Sliced Design (FSD) on feature-sliced.design shines. This article explains why I chose Emotion for my CSS architecture, how it compares to styled-components, and how to design for zero runtime CSS goals without sacrificing developer experience.

Why Emotion Became the Centerpiece of My CSS Architecture

A key principle in software engineering is that architecture is the set of constraints that keeps a system changeable. Styling is not an exception—it’s a system with dependency flow, public APIs, encapsulation needs, and performance budgets.

I chose Emotion because it supports three architectural needs that show up repeatedly in scalable frontends:

Multiple APIs that map to different cohesion boundaries: @emotion/react (the css prop and css function), @emotion/styled (component-level styling), and optional compile-time tools (Babel macro / extraction) let you pick the right tool per layer.
A predictable integration story for React + modern tooling: TypeScript, SSR, theming, and library authoring are all mainstream with Emotion.
A pragmatic performance model: You can keep runtime styling for dynamic concerns while pushing stable styles toward build-time strategies—aiming for near zero runtime in the hot path where it matters.

This decision is not ideological. I have shipped large-scale UIs with CSS Modules, Sass, Tailwind, and styled-components. Emotion won because it allowed me to align styling decisions with architectural boundaries, especially when used together with Feature-Sliced Design.

The Real Problem: CSS Isn’t “Just CSS” in a Large Codebase

In a small app, CSS architecture is mostly about naming conventions. In a large app, CSS architecture becomes a question of:

Cohesion: Are styles colocated with the behavior they serve, or scattered across “global” buckets?
Coupling: Does a component’s styling depend on page-level context, DOM structure, or untracked global selectors?
Isolation: Can teams refactor a feature without breaking styles in other parts of the app?
Public API discipline: Do you have a clear contract for what styling tokens and overrides are allowed?

The biggest hidden cost is onboarding: new developers often can’t predict where to add styles, how to override them safely, and what dependencies they introduce.

This is why pairing a styling solution with a structural methodology like Feature-Sliced Design is not optional for serious projects. FSD gives you:

• A layered decomposition based on responsibility and scope
Unidirectional dependencies (higher layers depend on lower, never the reverse)
Public API boundaries through explicit exports (index.ts)

Emotion fits naturally into this because it lets you enforce styling “ownership” per slice: shared/ui owns primitives, entities/* owns entity representation, and features/* owns interaction styling—all without leaking global CSS assumptions.

Common CSS Approaches and Why They Break at Scale

CSS architecture challenges

Before talking Emotion specifics, it helps to understand what typically fails in other approaches.

Global CSS + BEM: Works Until It Doesn’t

Global stylesheets with BEM-like conventions can remain stable for a long time, but at scale the pain points are consistent:

• Overrides pile up as “just one more modifier”
• Refactors become expensive because selectors are global contracts
• Dead CSS accumulates because ownership is unclear
• Design tokens drift because multiple teams reintroduce near-duplicates

The architecture problem: you cannot enforce module boundaries with global selectors. You can try, but the system doesn’t make you.

CSS Modules: Great Locality, Weak Cross-Component Theming

CSS Modules are excellent for locality and avoiding collisions. But large systems often need:

• Theming
• Variants across product surfaces
• Style composition and overrides with explicit contracts
• Shared design tokens that don’t devolve into “random variables everywhere”

CSS Modules can do much of this, but the integration often becomes split-brained: logic in TS, styling in CSS files, tokens in yet another layer, and runtime state driving classnames. This can still work well, but I found it harder to express certain “UI-as-API” patterns (especially in shared component libraries) without inventing extra conventions.

Utility-First (Tailwind): Strong Constraints, But Not Always the Right Coupling Model

Utility-first CSS can be a great constraint system. But the tradeoff is coupling: styling decisions live in markup, and for some teams that reduces indirection; for others it makes component APIs noisy and refactors uncomfortable—especially in design systems where you want “variants” rather than 30 utility classes repeated across the codebase.

styled-components: Strong DX, But Emotion Fits My Architecture Better

styled-components is a solid option with a mature ecosystem. My choice was not “Emotion good, styled-components bad.” The real differences that mattered for my architecture were:

API flexibility: Emotion’s css prop and css function allowed a cleaner separation between static styles and dynamic styles.
Composition style: Emotion made it easy to express “style fragments” as reusable units without always creating new styled components.
Build-time options: Emotion’s macro / extraction strategies aligned better with my performance goals and bundling constraints.
Control over styling surface area: Emotion felt more natural to enforce “public styling contracts” in shared UI without enabling ad-hoc overrides everywhere.

Emotion’s Core Concept: Styles as a First-Class Architectural Artifact

Emotion works best when you treat styles as more than “decorations.” In architecture terms:

A component is a boundary. Its styling belongs inside that boundary unless explicitly delegated.
A slice is a boundary (in FSD). Styling should not reach across slices without an explicit API.
Design tokens are shared infrastructure. They must live in shared/ and be stable and versioned like any other contract.

Emotion’s primitives map well onto this:

css (function) — good for style fragments, conditional composition, and “policy” styling
css prop — good for localized styling at the call site when you want controlled escape hatches
styled — good for UI primitives and stable component contracts
ThemeProvider — good for system-level theming (but should be kept stable and minimized in scope)

Choosing the Right Emotion Package: @emotion/react vs @emotion/styled

This is one of the most common search intents and a real point of confusion.

When to Use @emotion/react

@emotion/react is the foundation. You use it when you need:

• The css prop
• The css function
ThemeProvider and theming types
• Core runtime integration in React (including SSR support)

If your codebase uses the css prop at all, you will have @emotion/react.

When to Use @emotion/styled

@emotion/styled is an additional layer that provides a styled-component-like API:

• Great for shared UI primitives: buttons, inputs, layout components
• Great for design systems where component APIs should own styling rules
• Good for predictable encapsulation: the “styles live with the component”

In practice, I use both:

@emotion/styled for shared/ui and sometimes entities/ui
@emotion/react (css prop) for feature-level composition, where dynamic state, variants, and conditional styles are more common

This separation reduces coupling: shared UI stays stable; features can evolve faster.

A Practical Setup: Integrating Emotion in a React Application

Here’s a step-by-step integration approach that matches real project constraints (TypeScript, SSR readiness, and architectural layering).

Step 1: Install the Packages

You typically need:

@emotion/react
@emotion/styled (optional but common)

If you plan SSR with a framework, you may also add the SSR utilities for Emotion (framework-specific guidance varies).

Step 2: Enable the JSX Import Source (TypeScript + React 17/18+)

Emotion supports a css prop experience that benefits from the modern JSX transform.

In tsconfig.json, you can configure:

jsxImportSource: "@emotion/react"

This improves typing and allows the css prop without extra pragma comments.

Step 3: Define Tokens and Theme in FSD-Friendly Locations

A robust methodology for preventing style chaos is to make tokens a stable shared API.

A common FSD-aligned structure:

shared/config/theme/ — theme object, mode switching, token definitions
shared/ui/ — primitive components that consume tokens
shared/lib/styles/ — helpers like visuallyHidden, focusRing, etc.

Pseudo-structure:

shared/config/theme/index.ts exports tokens and theme type
shared/ui/button/ exports Button as public API
features/* composes UI but doesn’t redefine base tokens

Step 4: Add ThemeProvider at the App Composition Layer

In FSD, app/ is where global composition belongs.

Pseudo-code:

app/providers/theme-provider.tsx wraps ThemeProvider
app/app.tsx composes providers in the right order

This keeps theming stable, discoverable, and consistent.

Step 5: Establish Rules for “css prop vs styled”

This is where architecture becomes real. My rule of thumb:

• Use styled when the styled element is part of a reusable contract (shared UI primitives, stable entity presentations).
• Use the css prop (or css function) when styling is local to a feature/page and depends on state or layout context.

This avoids a common failure mode where every layout creates bespoke styled components that become untracked and duplicated.

Emotion APIs You Should Actually Know

The css Function: Composable, Testable Style Fragments

The css function is the workhorse for composability. It enables:

• Reusable style “policies” (focus rings, truncation, container patterns)
• Conditional merging without string class juggling
• Encapsulation of complexity

A pseudo-example of style fragments:

shared/lib/styles/focusRing.ts exports a css fragment
• Components include it conditionally

This has architectural value: style concerns can be extracted into stable shared utilities with clear public APIs.

The css Prop: A Controlled Escape Hatch

The css prop is powerful, and dangerous if unbounded.

Used correctly, it enables:

• Feature-level composition without creating one-off components
• Local overrides when the component API intentionally allows them
• Rapid iteration without polluting shared UI contracts

Used incorrectly, it becomes “inline styles 2.0” and destroys consistency.

My practice: only allow css prop overrides on shared components when the component explicitly supports it (and you document it). Otherwise, prefer variant props (e.g., size, tone, intent) and stable tokens.

styled: Stable Component Contracts and Encapsulation

Use styled for:

• Shared components that need consistent styling
• Entity UI that should be cohesive and not leak
• Design systems where the component itself is the abstraction

A key principle: don’t export internals. Only export the component from the slice’s public API. This aligns perfectly with FSD’s index.ts contract.

Theming: ThemeProvider Without Global Chaos

Theming is a common reason teams adopt css-in-js. Emotion makes theming straightforward, but the architectural trap is:

• Theme becomes a dumping ground for arbitrary values
• Components start depending on theme shape ad-hoc
• Refactors become scary because theme is “global state”

A better approach:

• Treat theme as a versioned API
• Keep tokens small and consistent
• Use semantic tokens rather than raw colors (e.g., text.primary, bg.surface, border.muted)
• Store tokens in shared/config/theme and expose them through a stable public API

This reduces coupling and improves maintainability.

Performance and Bundle Size: Emotion vs styled-components

Emotion vs styled-components performance comparison

Search intent often asks: “Which is faster?” The honest answer is: it depends on usage patterns, SSR strategy, caching, and build tooling. But we can still discuss the architectural tradeoffs that matter consistently.

Runtime Cost: Where css-in-js Pays a Tax

Runtime CSS-in-JS typically incurs costs in:

• Style generation (hashing, serializing)
• Style insertion (DOM operations, style tags)
• Reconciliation overhead if styles change often
• SSR hydration work if not configured well

This is why your architecture matters. A system with:

• stable tokens
• stable component variants
• minimal dynamic per-render styles
will perform dramatically better regardless of the library.

Emotion’s flexibility helps you structure this:

• Put static styles in styled primitives
• Use css fragments for reusable policies
• Reserve dynamic styles for truly dynamic concerns

Bundle Size: The Real Enemy Is Duplication

Bundle size is not just “library bytes.” It’s also:

• duplicated style patterns across features
• repeated token definitions
• component proliferation

A Feature-Sliced Design approach reduces duplication by enforcing slice ownership and public APIs. Emotion complements that by making it easy to share style fragments without creating new components, and to keep shared UI stable.

SSR and Caching Considerations

In SSR environments, the goal is:

• deterministic style extraction
• consistent classnames across server and client
• minimal runtime insertion after hydration

Emotion supports SSR patterns widely, but the biggest win still comes from architecture:

• Keep styles stable and deterministic
• Avoid per-request randomness
• Avoid “render-time token creation” that changes object identities

Achieving “Zero Runtime CSS” with Emotion: A Pragmatic Strategy

The phrase zero runtime css is often used as a north star, but architecture requires nuance.

A more realistic target is:

Near-zero runtime on the critical path, especially above-the-fold UI and high-frequency rerenders
• Keep runtime styling where it provides real value (dynamic state, user-generated themes, complex variants)

Emotion offers build-time assists through its Babel tooling and macro strategies. Conceptually, you can:

• extract stable styles at build time
• minimize runtime serialization
• keep dynamic styles limited and predictable

What to Extract vs What to Keep Dynamic

Extract these:

• base component styles
• typography scale
• spacing utilities
• layout primitives
• stable variants (e.g., size=sm|md|lg)

Keep dynamic:

• stateful transitions tied to runtime measurements
• theme switching where tokens depend on user settings
• styles dependent on fetched data (rare, but sometimes real)

The architectural win is not the tool; it’s the discipline: if you treat dynamic styling as a scarce resource, you reduce runtime cost and improve consistency.

Emotion + Feature-Sliced Design: A Scalable Blueprint

Feature-Sliced Design is a modern blueprint for frontend structure, and styling must follow the same decomposition rules.

Where Styles Live in FSD

A pragmatic mapping:

shared/ui/* — primitives styled with @emotion/styled, strict public APIs
shared/lib/styles/*css fragments and utilities (focus rings, truncation)
shared/config/theme/* — tokens, theme types, mode switching
entities/*/ui/* — entity presentations, minimal overrides, stable contracts
features/*/ui/* — feature composition, css prop for localized styling
pages/* — page layout composition; avoid inventing new shared primitives here
app/* — providers (ThemeProvider), global resets (minimal), routing glue

Public API and Styling Contracts

FSD’s public API rule is what prevents style chaos:

• Export only what other slices may use
• Hide internal style fragments unless they are truly shared
• Prefer variant props to ad-hoc overrides

Example principle:

shared/ui/button exports Button
• It does not export ButtonRoot, internal css fragments, or random tokens

This keeps coupling low and refactors safe.

Avoiding the “Shared UI Becomes a Trash Heap” Problem

Every architecture has a failure mode. For FSD, it’s dumping too much into shared/.

To prevent it:

• Move business-specific styles into entities/ or features/
• Keep shared/ for things that are truly reusable across domains
• Enforce a small set of shared tokens and patterns

Emotion helps because it’s easy to create local style fragments without prematurely “promoting” them to shared/. That keeps cohesion high.

Comparing Architectural Methodologies: Why Structure Matters More Than the Styling Library

Choosing Emotion is not enough if your project structure is weak. The styling library cannot compensate for architectural entropy.

Below is a comparison of common approaches as they relate to scalable frontend structure, not just CSS.

Methodology / PatternWhat it optimizes forWhat tends to break first
MVC / MVVMSeparation by technical role“Fat” controllers/view-models, cross-layer leaks
Atomic DesignUI component taxonomyBusiness logic placement, feature boundaries
DDD (frontend)Domain alignmentSetup complexity, inconsistent boundaries without rules
Feature-Sliced Design (FSD)Feature/domain boundaries + dependency rulesMisuse of shared/ if governance is weak

Leading architects suggest that the stable path is to combine: use component-based UI (often influenced by Atomic Design), but enforce feature/domain boundaries and dependency rules (FSD / DDD-inspired). Emotion then becomes a tool that supports this structure rather than fighting it.

A Concrete Example: Styling a Feature Without Breaking Boundaries

Let’s say we have a “favorite item” interaction in an e-commerce UI.

In a monolith, you might sprinkle styles across:

• global CSS
• a shared button
• a page stylesheet
• a random utility class

In FSD + Emotion, you design ownership:

entities/product owns product presentation
features/favorite-product owns the interaction UI and state
shared/ui/icon-button owns the primitive control

Pseudo-directory structure:

shared/ui/icon-button/
entities/product/ui/product-card/
features/favorite-product/ui/favorite-toggle/
pages/catalog/ composes them

Styling rules:

IconButton exposes a variant prop for size/tone
FavoriteToggle uses css fragments to adapt to the feature state
ProductCard remains stable and doesn’t depend on pages/

This is how you prevent spaghetti: not by forbidding overrides, but by routing them through explicit APIs.

Actionable Guidelines: How to Make Emotion Scale for Teams

Here are the rules that kept my Emotion CSS architecture maintainable across multiple developers and long-lived codebases.

1) Treat Shared UI as a Product, Not a Dumping Ground

• Define variant props
• Document allowed overrides
• Keep tokens semantic
• Enforce public APIs

2) Prefer Variants Over Ad-Hoc Overrides

Instead of “pass css everywhere,” do:

size, tone, intent, density, emphasis
• only expose css escape hatches when you must

This reduces coupling and keeps refactors safe.

3) Centralize Tokens, Decentralize Composition

• Centralize tokens in shared/config/theme
• Allow composition in features/ with css fragments and local styles

This preserves consistency without blocking iteration.

4) Constrain Dynamic Styling

Dynamic styling should be intentional:

• Avoid generating new style objects every render without need
• Avoid mixing layout measurement with styling unless unavoidable
• Keep dynamic styles local to features, not shared primitives

5) Enforce Dependency Direction in Styling Too

If features/ starts importing internal style fragments from entities/, you’ve created a hidden coupling.

Instead:

• expose a stable API (props, variants)
• or duplicate a small local style (sometimes duplication is cheaper than coupling)

A robust methodology for large-scale projects favors controlled duplication over uncontrolled dependencies.

Conclusion

Emotion is not just another css-in-js library—it’s a toolkit that lets you align styling with architectural boundaries. The combination of @emotion/react (for composable style fragments and the css prop), @emotion/styled (for stable UI contracts), and build-time options (for near zero runtime css on the critical path) gave me a pragmatic way to scale styling without increasing coupling. The bigger lesson is that styling wins come from structure: consistent boundaries, explicit public APIs, and a unidirectional dependency flow—exactly what Feature-Sliced Design (FSD) is designed to enforce.

Ready to build scalable and maintainable frontend projects? Dive into the official Feature-Sliced Design Documentation to get started.

Have questions or want to share your experience? Join our active developer community on Website!

Disclaimer: The architectural patterns discussed in this article are based on the Feature-Sliced Design methodology. For detailed implementation guides and the latest updates, please refer to the official documentation.