Styled Components & CSS-in-JS
CSS-in-JS is the approach of writing your CSS directly inside JavaScript files. The most influential library in this space is Styled Components, which introduced the tagged template literal syntax that the entire ecosystem later adopted. Understanding CSS-in-JS is important even if you choose not to use it — it shaped how the React community thinks about component-scoped styles and dynamic theming.
Installation
npm install styled-components npm install --save-dev @types/styled-components
The Core Syntax
styled.tagName is a tagged template literal function. You write CSS inside the backticks exactly as you would in a stylesheet. Styled Components returns a real React component that renders the HTML element with those styles applied via a generated class name.
import styled from 'styled-components'
// Creates a <button> element with these styles
const Button = styled.button`
display: inline-flex;
align-items: center;
padding: 8px 18px;
font-size: 0.875rem;
font-weight: 600;
border-radius: 6px;
border: none;
cursor: pointer;
background: #0070f3;
color: white;
transition: background 0.15s ease;
&:hover {
background: #0051a2;
}
&:active {
transform: scale(0.97);
}
`
// Usage — it's just a React component
function App() {
return <Button onClick={() => console.log('clicked')}>Save</Button>
}Props-Based Dynamic Styling
This is where CSS-in-JS genuinely shines over CSS Modules. You can interpolate functions inside the template literal that receive the component's props, making dynamic styles feel completely natural:
import styled from 'styled-components'
interface ButtonProps {
variant?: 'primary' | 'secondary' | 'danger'
size?: 'sm' | 'md' | 'lg'
fullWidth?: boolean
}
const Button = styled.button<ButtonProps>`
display: inline-flex;
align-items: center;
justify-content: center;
font-weight: 600;
border: none;
border-radius: 6px;
cursor: pointer;
width: ${(props) => (props.fullWidth ? '100%' : 'auto')};
/* Size variants */
padding: ${({ size }) => {
if (size === 'sm') return '4px 10px'
if (size === 'lg') return '12px 24px'
return '8px 18px' /* md (default) */
}};
font-size: ${({ size }) => (size === 'sm' ? '0.75rem' : size === 'lg' ? '1rem' : '0.875rem')};
/* Color variants */
background: ${({ variant }) => {
if (variant === 'secondary') return 'transparent'
if (variant === 'danger') return '#dc2626'
return '#0070f3' /* primary (default) */
}};
color: ${({ variant }) => (variant === 'secondary' ? '#0070f3' : 'white')};
border: ${({ variant }) => variant === 'secondary' ? '1.5px solid #0070f3' : 'none'};
&:hover {
opacity: 0.9;
}
`
function App() {
return (
<div>
<Button>Primary (default)</Button>
<Button variant="secondary" size="sm">Secondary Small</Button>
<Button variant="danger" size="lg" fullWidth>Delete Account</Button>
</div>
)
}Theming with ThemeProvider
Styled Components ships a ThemeProvider that injects a theme object into every styled component via React context. This is the canonical pattern for design tokens in CSS-in-JS:
import { ThemeProvider, createGlobalStyle } from 'styled-components'
// Define your theme shape
const theme = {
colors: {
primary: '#0070f3',
danger: '#dc2626',
text: '#111827',
background: '#ffffff',
surface: '#f9fafb',
},
spacing: {
sm: '8px',
md: '16px',
lg: '24px',
},
radius: {
sm: '4px',
md: '6px',
lg: '12px',
},
}
type Theme = typeof theme
// Augment the DefaultTheme so TypeScript knows the shape
declare module 'styled-components' {
export interface DefaultTheme extends Theme {}
}
// Now every styled component receives the theme via props.theme
const Card = styled.div`
background: ${({ theme }) => theme.colors.surface};
padding: ${({ theme }) => theme.spacing.md};
border-radius: ${({ theme }) => theme.radius.lg};
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
`
const Title = styled.h2`
color: ${({ theme }) => theme.colors.text};
margin: 0 0 ${({ theme }) => theme.spacing.sm};
`
function App() {
return (
<ThemeProvider theme={theme}>
<Card>
<Title>Hello, world</Title>
</Card>
</ThemeProvider>
)
}Global Styles with createGlobalStyle
import { createGlobalStyle } from 'styled-components'
const GlobalStyle = createGlobalStyle`
*, *::before, *::after {
box-sizing: border-box;
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: ${({ theme }) => theme.colors.background};
color: ${({ theme }) => theme.colors.text};
}
`
function App() {
return (
<ThemeProvider theme={theme}>
<GlobalStyle />
<main>...</main>
</ThemeProvider>
)
}Keyframe Animations
import styled, { keyframes } from 'styled-components'
const fadeIn = keyframes`
from { opacity: 0; transform: translateY(-8px); }
to { opacity: 1; transform: translateY(0); }
`
const spin = keyframes`
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
`
const Toast = styled.div`
animation: ${fadeIn} 0.2s ease-out;
background: #111;
color: white;
padding: 12px 16px;
border-radius: 8px;
`
const Spinner = styled.div`
width: 24px;
height: 24px;
border: 3px solid #e5e7eb;
border-top-color: #0070f3;
border-radius: 50%;
animation: ${spin} 0.8s linear infinite;
`The .attrs() Helper
.attrs() lets you attach default HTML attributes to a styled component, which avoids repeating them at every usage site:
// Without .attrs() you'd write type="button" everywhere
const Button = styled.button.attrs({ type: 'button' })`
padding: 8px 16px;
`
// Useful for inputs with default attributes
const TextInput = styled.input.attrs({ type: 'text', autoComplete: 'off' })`
border: 1px solid #d1d5db;
border-radius: 4px;
padding: 8px 12px;
`SSR with Next.js
Styled Components injects styles at runtime. Without SSR setup, the server sends unstyled HTML and the styles flash in after hydration. In Next.js (App Router) install the official plugin:
npm install --save-dev babel-plugin-styled-components
// next.config.js
const nextConfig = {
compiler: {
styledComponents: true, // enables the SWC transform — no Babel needed
},
}
module.exports = nextConfigEmotion: The Alternative
Emotion is a direct Styled Components competitor with a nearly identical API. It is the styling engine behind MUI (Material UI) and Chakra UI. Emotion offers two entry points: @emotion/styled (same API as Styled Components) and @emotion/css (framework-agnostic). Emotion's compile-time extraction mode (@emotion/babel-plugin) is more mature than Styled Components', but the runtime cost remains.
Trade-off Summary
Concern | CSS-in-JS verdict |
|---|---|
Co-location | Excellent — styles and logic in one file |
Dynamic styling | First-class — interpolate any JS expression |
Theming | Excellent — ThemeProvider + typed theme |
Runtime cost | Real — style tag injection on every render |
SSR | Needs extra setup — ServerStyleSheet or compiler plugin |
Server Components (React 19) | Incompatible with runtime injection |
Bundle size | Adds ~12–30 KB to client bundle |
Industry trend | Declining — teams moving to Tailwind / CSS Modules |
Use Styled Components only in existing codebases that already rely on it — refactoring away is a gradual process
Prefer CSS Modules or Tailwind for new projects — no runtime cost, SSR-safe, React 19-compatible
Use Emotion if you adopt MUI — it is baked into their rendering pipeline already
Typed themes via
DefaultThemeaugmentation are one of CSS-in-JS's best features — worth borrowing as a CSS custom property approach even in non-CSS-in-JS projects