What is CSS-in-JS?
CSS-in-JS is a styling approach where CSS is written directly within JavaScript (or TypeScript) files, rather than maintaining separate .css stylesheets. Instead of globally scoped class names and cascading rules, styles are scoped to individual components, co-located with the logic that drives them. This pattern gained traction alongside component-based UI libraries like React, where the mental model of bundling markup, logic, and styling into a single reusable unit proved remarkably intuitive.
At its core, CSS-in-JS solves fundamental problems that plagued traditional stylesheets at scale: global namespace collisions, dead code elimination difficulties, and the disconnect between a component's behavior and its visual presentation. Libraries like Styled Components, Emotion, and CSS Modules (which straddles the line between traditional CSS and the CSS-in-JS philosophy) each tackle these challenges with slightly different APIs and mechanisms.
Why CSS-in-JS Matters
🚀 Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
Try it free →The value of CSS-in-JS extends beyond mere convenience. Here are the critical benefits that make it an essential tool in modern front-end development:
- True scoping without side effects: Styles are automatically scoped to a component, eliminating the fear of one selector leaking into another component's DOM. No more hunting down rogue
.cardclasses that accidentally style cards across the entire application. - Dynamic styling based on props and state: Because styles live in JavaScript, they can react to component props, theme values, or application state. A button can change its background color based on a
variantprop without toggling a dozen CSS classes. - Dead code elimination: Styles tied to components are automatically removed when the component is no longer imported anywhere. No orphaned CSS rules lingering in a 5000-line stylesheet.
- Co-location of concerns: When you open a component file, everything you need — markup, event handlers, and styling — lives in one place. This reduces cognitive overhead and speeds up development and debugging.
- Vendor prefixing and optimizations: Most CSS-in-JS libraries handle vendor prefixes automatically and apply critical optimizations like merging duplicate rules at runtime.
Styled Components: The Tagged Template Literal Approach
Styled Components is arguably the most iconic CSS-in-JS library. It uses tagged template literals to define styles that map directly to DOM elements, creating a new styled component in the process. The library automatically generates unique class names and injects styles into the document head.
Installation and Basic Usage
npm install styled-components
Here's a practical example — a versatile Button component with variants driven entirely by props:
import styled from 'styled-components';
const Button = styled.button`
font-family: 'Inter', sans-serif;
font-weight: 600;
border-radius: 8px;
padding: 12px 24px;
border: 2px solid transparent;
cursor: pointer;
transition: all 0.2s ease;
font-size: ${props => props.size === 'small' ? '14px' : '16px'};
/* Variant-based styling */
background: ${props => {
switch(props.variant) {
case 'primary':
return '#6366f1';
case 'secondary':
return '#64748b';
case 'danger':
return '#ef4444';
default:
return '#6366f1';
}
}};
color: ${props => props.variant === 'ghost' ? '#6366f1' : 'white'};
background: ${props => props.variant === 'ghost' && 'transparent'};
border-color: ${props => props.variant === 'ghost' && '#6366f1'};
&:hover {
opacity: ${props => props.disabled ? 1 : 0.85};
transform: ${props => props.disabled ? 'none' : 'translateY(-1px)'};
}
&:active {
transform: ${props => props.disabled ? 'none' : 'translateY(0)'};
}
${props => props.disabled && `
opacity: 0.5;
cursor: not-allowed;
pointer-events: none;
`}
`;
// Usage in a component
function App() {
return (
);
}
Extending Styles and Theming
Styled Components allows you to extend existing styled components, keeping your design system DRY. It also provides a powerful ThemeProvider that injects a theme object into all descendant styled components via context.
import styled, { ThemeProvider } from 'styled-components';
const BaseCard = styled.div`
background: white;
border-radius: 12px;
padding: 24px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
`;
// Extend BaseCard with additional or overriding styles
const FeaturedCard = styled(BaseCard)`
box-shadow: 0 8px 24px rgba(99, 102, 241, 0.25);
border-left: 4px solid #6366f1;
`;
const theme = {
colors: {
primary: '#6366f1',
secondary: '#64748b',
background: '#f8fafc'
},
spacing: {
small: '8px',
medium: '16px',
large: '24px'
}
};
const ThemedButton = styled.button`
background: ${props => props.theme.colors.primary};
padding: ${props => props.theme.spacing.medium};
color: white;
border: none;
border-radius: 8px;
`;
// Wrap your app
function App() {
return (
A Themed Button
);
}
Emotion: The Flexible Powerhouse
Emotion is a high-performance CSS-in-JS library that offers two distinct APIs: the familiar styled API (similar to Styled Components) and the css prop API, which lets you apply styles directly to elements without creating named styled components. This flexibility makes Emotion particularly powerful for rapid prototyping and for teams that prefer composable style objects.
Installation and the css Prop
npm install @emotion/react @emotion/styled
To use the css prop with React, you need to configure the JSX pragma. With modern Create React App or Vite setups, this is handled automatically. For manual setups, add this to your Babel config or use the jsxImportSource in tsconfig. Here's a practical example using the css prop with dynamic styles:
/** @jsxImportSource @emotion/react */
import { css } from '@emotion/react';
function NotificationBanner({ type, message }) {
const bannerStyle = css`
padding: 16px 24px;
border-radius: 8px;
margin-bottom: 16px;
display: flex;
align-items: center;
font-weight: 500;
/* Dynamic styling based on type prop */
background: ${type === 'success' ? '#ecfdf5' : type === 'error' ? '#fef2f2' : '#eff6ff'};
color: ${type === 'success' ? '#065f46' : type === 'error' ? '#991b1b' : '#1e40af'};
border-left: 4px solid ${type === 'success' ? '#10b981' : type === 'error' ? '#ef4444' : '#3b82f6'};
&:hover {
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
}
`;
return (
{message}
);
}
The styled API and Composing Styles
Emotion's styled API works almost identically to Styled Components but includes additional composition utilities. One standout feature is cx for combining class names and the ability to compose multiple style objects cleanly.
import styled from '@emotion/styled';
import { css, cx } from '@emotion/react';
// Define reusable style fragments
const flexCenter = css`
display: flex;
align-items: center;
justify-content: center;
`;
const cardBase = css`
background: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
`;
// Compose them together
const DashboardWidget = styled.div`
${cardBase};
${flexCenter};
flex-direction: column;
padding: 32px;
min-height: 200px;
/* Access theme via props.theme */
background: ${props => props.theme.colors.surface};
`;
// The cx utility for combining classes conditionally
function Widget({ children, highlighted }) {
const widgetClasses = cx(
'dashboard-widget', // external class
{ 'highlighted': highlighted },
highlighted && 'animate-pulse'
);
return {children};
}
Object Styles Syntax
Emotion fully supports defining styles as JavaScript objects, which TypeScript users often prefer for superior type checking and autocompletion:
import { css } from '@emotion/react';
const cardStyles = css({
backgroundColor: '#ffffff',
borderRadius: '12px',
boxShadow: '0 2px 8px rgba(0,0,0,0.08)',
padding: '24px',
transition: 'box-shadow 0.2s ease',
'&:hover': {
boxShadow: '0 8px 24px rgba(0,0,0,0.12)',
},
'& .card-title': {
fontSize: '18px',
fontWeight: 600,
marginBottom: '8px',
}
});
// Works with nested selectors and pseudo-classes
const interactiveStyles = css({
'&:focus-visible': {
outline: '2px solid #6366f1',
outlineOffset: '2px',
},
'&::after': {
content: '""',
display: 'block',
width: '100%',
height: '2px',
background: 'linear-gradient(to right, #6366f1, #8b5cf6)',
}
});
CSS Modules: The Native Approach
CSS Modules offer a middle ground: you write standard .css or .scss files, but the build tool (Webpack, Vite, Next.js, etc.) automatically scopes class names by generating unique identifiers. The styles are then imported as a JavaScript object mapping original class names to the generated unique names. This approach preserves the full power of CSS (cascade, pseudo-classes, media queries, post-processing) while guaranteeing component-level isolation.
Setup with Vite or Next.js
Both Vite and Next.js support CSS Modules out of the box — no additional configuration needed. Simply name your file with the .module.css extension.
/* Button.module.css */
.button {
font-family: 'Inter', sans-serif;
font-weight: 600;
border-radius: 8px;
padding: 12px 24px;
border: none;
cursor: pointer;
transition: all 0.2s ease;
background: #6366f1;
color: white;
}
.button:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.4);
}
.button:active {
transform: translateY(0);
}
.primary {
background: #6366f1;
}
.secondary {
background: #64748b;
}
.danger {
background: #ef4444;
}
.disabled {
opacity: 0.5;
cursor: not-allowed;
pointer-events: none;
}
/* Compose multiple classes */
.outline {
background: transparent;
border: 2px solid #6366f1;
color: #6366f1;
}
// Button.jsx
import styles from './Button.module.css';
import { clsx } from 'clsx'; // tiny utility for combining classes
function Button({ variant = 'primary', disabled, outline, children }) {
const className = clsx(
styles.button,
styles[variant], // dynamic class based on prop
{ [styles.disabled]: disabled },
{ [styles.outline]: outline }
);
return (
);
}
CSS Modules Composition and Global Selectors
CSS Modules support a composes property that lets you inherit styles from other classes, even across different module files. You can also selectively opt into global scope with :global().
/* Card.module.css */
.card {
background: white;
border-radius: 12px;
padding: 24px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
composes: fadeIn from './animations.module.css';
}
.featuredCard {
composes: card;
border-left: 4px solid #6366f1;
box-shadow: 0 8px 24px rgba(99, 102, 241, 0.25);
}
/* Targeting a global class from within a module */
.root :global(.dark-theme) & {
background: #1e293b;
color: #e2e8f0;
}
Choosing the Right Tool: A Comparison Guide
The choice between Styled Components, Emotion, and CSS Modules depends on your team's workflow, performance requirements, and stylistic preferences. Here's a practical breakdown:
- Styled Components excels when you want a mature, widely-adopted library with excellent documentation and a large community. Its
ThemeProviderintegration and straightforward styled API make it ideal for design systems with heavy theming requirements. The tagged template syntax reads beautifully for complex CSS with nested selectors. - Emotion shines when you need flexibility. Its
cssprop allows inline styling without creating new component instances, which can reduce overhead in render-heavy scenarios. The object syntax support makes it a strong choice for TypeScript-heavy codebases. Emotion also offers slightly better runtime performance in benchmarks due to its aggressive caching strategy. - CSS Modules is the best choice when you want to stay close to standard CSS, leverage existing PostCSS plugins, or avoid runtime JavaScript overhead entirely. It's zero-cost at runtime — the module resolution happens at build time. This makes it perfect for performance-critical applications and teams with designers who prefer working with actual CSS files.
Best Practices for CSS-in-JS at Scale
1. Establish a Design Tokens System
Before writing any component styles, define your design tokens — colors, spacing, typography scales, breakpoints — as JavaScript constants. These become the single source of truth that feeds into your theme object.
// tokens.js
export const tokens = {
colors: {
primary: {
50: '#eef2ff',
500: '#6366f1',
900: '#312e81',
},
neutral: {
100: '#f1f5f9',
500: '#64748b',
900: '#0f172a',
},
},
spacing: {
xs: '4px',
sm: '8px',
md: '16px',
lg: '24px',
xl: '32px',
'2xl': '48px',
},
breakpoints: {
sm: '640px',
md: '768px',
lg: '1024px',
xl: '1280px',
},
typography: {
fontFamily: "'Inter', -apple-system, BlinkMacSystemFont, sans-serif",
scale: {
sm: '0.875rem',
base: '1rem',
lg: '1.125rem',
xl: '1.25rem',
'2xl': '1.5rem',
}
}
};
2. Avoid Unnecessary Component Creation
Every styled.div creates a new React component. For highly dynamic interfaces, prefer the css prop (Emotion) or style functions that return class strings rather than spawning hundreds of styled components. Reserve styled components for truly reusable design system primitives.
// ❌ Avoid: Creating a new styled component for every variant combination
const RedButton = styled.button`background: red;`;
const BlueButton = styled.button`background: blue;`;
const GreenButton = styled.button`background: green;`;
// ✅ Prefer: Single component that accepts props
const Button = styled.button`
background: ${props => props.color || '#6366f1'};
`;
// ✅ Or use css prop for one-off styled elements
function AdHocBanner({ children }) {
return {children};
}
3. Co-locate Styles, But Extract When Patterns Emerge
Keep styles in the same file as the component that uses them. When you notice the same pattern appearing in three or more places, extract it into a shared styled component or a style utility function. This prevents premature abstraction while maintaining DRY principles over time.
// Extracted utility for common flex patterns
import { css } from '@emotion/react';
export const flexLayout = {
center: css`
display: flex;
align-items: center;
justify-content: center;
`,
between: css`
display: flex;
align-items: center;
justify-content: space-between;
`,
stack: css`
display: flex;
flex-direction: column;
`
};
// Usage in any component
function Header() {
return (
Logo
);
}
4. Handle Responsive Design Systematically
Define breakpoint utilities that work across your styling approach. For Styled Components and Emotion, create media query helpers. For CSS Modules, use custom media queries or PostCSS plugins.
// responsive-helpers.js
import { tokens } from './tokens';
export const breakpoints = tokens.breakpoints;
// For Styled Components / Emotion tagged templates
export const media = {
sm: `@media (min-width: ${breakpoints.sm})`,
md: `@media (min-width: ${breakpoints.md})`,
lg: `@media (min-width: ${breakpoints.lg})`,
};
// Usage in styled components
const ResponsiveGrid = styled.div`
display: grid;
grid-template-columns: 1fr;
gap: 16px;
${media.sm} {
grid-template-columns: repeat(2, 1fr);
}
${media.lg} {
grid-template-columns: repeat(3, 1fr);
gap: 24px;
}
`;
// For Emotion object syntax
const gridStyles = css({
display: 'grid',
gridTemplateColumns: '1fr',
gap: '16px',
[`@media (min-width: ${breakpoints.md})`]: {
gridTemplateColumns: 'repeat(2, 1fr)',
},
[`@media (min-width: ${breakpoints.lg})`]: {
gridTemplateColumns: 'repeat(3, 1fr)',
gap: '24px',
}
});
5. Keep Selector Specificity Predictable
Avoid deeply nested selectors. With CSS-in-JS, a component's styles should primarily target its own root element and immediate children. For deeply nested descendant styling, consider breaking the component into smaller pieces. This maintains the single-responsibility principle and prevents specificity wars.
// ❌ Avoid: Deep nesting that couples styles to DOM structure
const Sidebar = styled.div`
& > ul {
list-style: none;
& > li {
padding: 8px;
& > a {
color: #64748b;
& > span {
font-weight: 500;
}
}
}
}
`;
// ✅ Prefer: Compose smaller styled components
const SidebarList = styled.ul`list-style: none;`;
const SidebarItem = styled.li`padding: 8px;`;
const SidebarLink = styled.a`color: #64748b;`;
const LinkLabel = styled.span`font-weight: 500;`;
6. Leverage the Theme for Consistency
Never hardcode colors, spacing, or typography values inside component styles. Always reference the theme. This ensures that a design refresh touches only the theme object, not hundreds of component files.
// ❌ Hardcoded values
const BadButton = styled.button`
background: #6366f1;
padding: 16px;
border-radius: 8px;
`;
// ✅ Theme-referenced values
const GoodButton = styled.button`
background: ${props => props.theme.colors.primary[500]};
padding: ${props => props.theme.spacing.md};
border-radius: ${props => props.theme.borderRadius};
`;
7. Optimize Runtime Performance
For Styled Components and Emotion, be mindful that interpolations run on every render. Use the attrs pattern or memoize expensive style computations when dealing with frequently re-rendering components.
import styled from 'styled-components';
// Use attrs for static props that shouldn't trigger re-computation
const Input = styled.input.attrs(props => ({
type: props.type || 'text',
placeholder: props.placeholder || 'Enter value...',
}))`
padding: 12px;
border: 2px solid #e2e8f0;
border-radius: 8px;
&:focus {
border-color: #6366f1;
outline: none;
}
`;
// For Emotion, use the css cache to avoid repeated object creation
import { css } from '@emotion/react';
function ExpensiveList({ items }) {
// This style object is recreated on every render if defined inline
// Extract it to module scope or use useMemo for dynamic parts
const itemStyle = css({
padding: '8px 12px',
marginBottom: '4px',
borderRadius: '6px',
'&:hover': { background: '#f1f5f9' }
});
return items.map(item => {item.label});
}
8. Debugging with Proper Source Maps and Babel Plugins
Styled Components offers a Babel plugin that adds display names and meaningful class names during development. Emotion provides similar functionality. Always enable these for a sane debugging experience.
// .babelrc or babel.config.js
{
"plugins": [
"babel-plugin-styled-components",
// or for Emotion
"@emotion/babel-plugin"
]
}
// With the Styled Components plugin, your component
// will show as
9. CSS Modules + TypeScript: Type-Safe Class Names
When using CSS Modules with TypeScript, generate type definitions to get autocompletion on your imported styles object. Packages like typed-css-modules or built-in support in Next.js make this seamless.
// Generated file: Button.module.css.d.ts
declare const styles: {
readonly button: string;
readonly primary: string;
readonly secondary: string;
readonly danger: string;
readonly disabled: string;
readonly outline: string;
};
export default styles;
// Now in your component you get full type safety:
import styles from './Button.module.css';
// styles.button ✅ autocompleted
// styles.nonexistent ❌ TypeScript error
10. Testing Components with CSS-in-JS
When testing, avoid asserting on generated class names (they change between builds). Instead, use accessible selectors, test IDs, or snapshot testing with proper serializer configuration.
// jest.config.js for Styled Components
{
"snapshotSerializers": ["jest-styled-components"]
}
// Test example using Testing Library
import { render, screen } from '@testing-library/react';
import { Button } from './Button';
test('applies disabled styles when disabled prop is true', () => {
render();
const button = screen.getByRole('button', { name: /submit/i });
// Assert on behavior, not class names
expect(button).toBeDisabled();
expect(button).toHaveStyle('cursor: not-allowed');
});
Real-World Migration Strategy
If you're adopting CSS-in-JS in an existing codebase, avoid the "big bang rewrite." Migrate incrementally: start with new components using your chosen library, convert frequently modified components next, and leave legacy stylesheets intact until they naturally phase out. All three approaches — Styled Components, Emotion, and CSS Modules — can coexist with traditional stylesheets, making gradual adoption painless.
// Coexistence example: A new component using Styled Components
// alongside existing global CSS classes
import styled from 'styled-components';
const NewFeatureCard = styled.div`
background: white;
border-radius: 12px;
padding: 24px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
`;
function HybridComponent() {
return (
{/* New styled component */}
{/* Legacy class from global stylesheet still works */}
Feature Title
Feature description here.
);
}
Conclusion
Mastering CSS-in-JS is ultimately about understanding the trade-offs each approach offers and developing the discipline to use them effectively at scale. Styled Components provides a polished, component-first experience with excellent theming support. Emotion offers unmatched flexibility with its dual API and object syntax, making it a strong fit for TypeScript projects and performance-conscious teams. CSS Modules delivers the familiarity and zero-runtime cost of standard CSS while guaranteeing local scope — an ideal choice when you want the platform's full styling capabilities without JavaScript overhead.
The best practices outlined here — establishing design tokens, avoiding unnecessary component creation, extracting patterns judiciously, maintaining predictable specificity, always referencing the theme, optimizing runtime performance, and enabling proper debugging tools — will serve you regardless of which library you choose. The goal is not to pick the "perfect" library but to build a styling architecture that scales gracefully with your application, keeps your codebase maintainable, and empowers your team to ship consistent, performant user interfaces with confidence.