Emotion CSS-in-JS Complete Guide | Styled Components Alternative
이 글의 핵심
Emotion lets you write CSS directly in JavaScript with full TypeScript support, theming, and dynamic styles. This guide covers both the css prop and styled API, plus theming, SSR, and when to use Emotion vs Tailwind CSS.
Why Emotion?
Emotion lets you colocate styles with components, use JavaScript variables in CSS, and get TypeScript autocomplete for theme values:
// Before: separate CSS file
// styles.module.css → .button { ... }
// component.tsx → className="button"
// After: Emotion
const Button = styled.button`
background: ${theme.colors.primary};
padding: ${theme.spacing(2)};
&:hover { background: ${theme.colors.primaryDark}; }
`
Installation
# Core packages
npm install @emotion/react @emotion/styled
# Optional: babel plugin for css prop (without pragma comment)
npm install --save-dev @emotion/babel-plugin
1. Two APIs
Emotion has two main APIs:
@emotion/styled — Like styled-components
import styled from '@emotion/styled'
const Button = styled.button`
background: cornflowerblue;
color: white;
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
&:hover {
background: royalblue;
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
`
// Usage
<Button onClick={handleClick}>Click Me</Button>
<Button disabled>Disabled</Button>
@emotion/react — css prop
/** @jsxImportSource @emotion/react */
import { css } from '@emotion/react'
const buttonStyles = css`
background: cornflowerblue;
color: white;
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
`
// css prop works on any JSX element
function App() {
return (
<button css={buttonStyles}>Click Me</button>
)
}
2. Dynamic Styles
import styled from '@emotion/styled'
interface ButtonProps {
variant?: 'primary' | 'secondary' | 'danger'
size?: 'sm' | 'md' | 'lg'
fullWidth?: boolean
}
const Button = styled.button<ButtonProps>`
border: none;
border-radius: 4px;
cursor: pointer;
width: ${({ fullWidth }) => fullWidth ? '100%' : 'auto'};
/* Variant styles */
${({ variant = 'primary' }) => {
const variants = {
primary: `background: cornflowerblue; color: white;`,
secondary: `background: transparent; color: cornflowerblue; border: 1px solid currentColor;`,
danger: `background: #e53e3e; color: white;`,
}
return variants[variant]
}}
/* Size styles */
${({ size = 'md' }) => {
const sizes = {
sm: `padding: 4px 8px; font-size: 12px;`,
md: `padding: 8px 16px; font-size: 14px;`,
lg: `padding: 12px 24px; font-size: 16px;`,
}
return sizes[size]
}}
`
// Usage
<Button variant="primary" size="lg">Submit</Button>
<Button variant="danger" fullWidth>Delete Account</Button>
3. Theming
import { ThemeProvider, useTheme } from '@emotion/react'
import styled from '@emotion/styled'
// Define theme type
declare module '@emotion/react' {
export interface Theme {
colors: {
primary: string
primaryDark: string
background: string
text: string
border: string
}
spacing: (n: number) => string
borderRadius: string
shadows: {
sm: string
md: string
}
}
}
const lightTheme = {
colors: {
primary: '#4A90E2',
primaryDark: '#357ABD',
background: '#ffffff',
text: '#1a1a1a',
border: '#e2e8f0',
},
spacing: (n: number) => `${n * 8}px`,
borderRadius: '6px',
shadows: {
sm: '0 1px 3px rgba(0,0,0,0.12)',
md: '0 4px 6px rgba(0,0,0,0.1)',
},
}
const darkTheme = {
...lightTheme,
colors: {
primary: '#63B3ED',
primaryDark: '#4299E1',
background: '#1a202c',
text: '#e2e8f0',
border: '#2d3748',
},
}
// Theme-aware styled component
const Card = styled.div`
background: ${({ theme }) => theme.colors.background};
color: ${({ theme }) => theme.colors.text};
border: 1px solid ${({ theme }) => theme.colors.border};
border-radius: ${({ theme }) => theme.borderRadius};
padding: ${({ theme }) => theme.spacing(3)};
box-shadow: ${({ theme }) => theme.shadows.sm};
`
const PrimaryButton = styled.button`
background: ${({ theme }) => theme.colors.primary};
color: white;
padding: ${({ theme }) => theme.spacing(1)} ${({ theme }) => theme.spacing(2)};
border: none;
border-radius: ${({ theme }) => theme.borderRadius};
&:hover {
background: ${({ theme }) => theme.colors.primaryDark};
}
`
// useTheme hook in function components
function Header() {
const theme = useTheme()
return (
<header css={{ background: theme.colors.primary, padding: theme.spacing(2) }}>
Logo
</header>
)
}
// App root
function App() {
const [isDark, setIsDark] = useState(false)
return (
<ThemeProvider theme={isDark ? darkTheme : lightTheme}>
<Card>
<PrimaryButton onClick={() => setIsDark(!isDark)}>
Toggle Theme
</PrimaryButton>
</Card>
</ThemeProvider>
)
}
4. Composition
import { css } from '@emotion/react'
import styled from '@emotion/styled'
// Reusable style fragments
const flexCenter = css`
display: flex;
align-items: center;
justify-content: center;
`
const truncate = css`
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
`
// Compose multiple styles
const Card = styled.div`
${flexCenter}
padding: 16px;
border-radius: 8px;
`
// Extend another styled component
const HighlightedCard = styled(Card)`
border: 2px solid cornflowerblue;
background: rgba(100, 149, 237, 0.1);
`
// Compose with css prop
function UserName({ children }: { children: string }) {
return (
<span css={[truncate, { maxWidth: '200px', display: 'block' }]}>
{children}
</span>
)
}
5. Keyframe Animations
import { keyframes } from '@emotion/react'
import styled from '@emotion/styled'
const fadeIn = keyframes`
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
`
const spin = keyframes`
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
`
const pulse = keyframes`
0%, 100% { transform: scale(1); }
50% { transform: scale(1.05); }
`
const FadeInDiv = styled.div`
animation: ${fadeIn} 0.3s ease-out;
`
const Spinner = styled.div`
width: 24px;
height: 24px;
border: 2px solid #e2e8f0;
border-top-color: cornflowerblue;
border-radius: 50%;
animation: ${spin} 0.8s linear infinite;
`
const PulseButton = styled.button`
animation: ${pulse} 2s ease-in-out infinite;
`
6. Global Styles
import { Global, css } from '@emotion/react'
const globalStyles = css`
*, *::before, *::after {
box-sizing: border-box;
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
-webkit-font-smoothing: antialiased;
}
h1, h2, h3, h4, h5, h6 {
margin: 0 0 1rem;
line-height: 1.2;
}
a {
color: cornflowerblue;
text-decoration: none;
&:hover { text-decoration: underline; }
}
`
function App() {
return (
<>
<Global styles={globalStyles} />
{/* rest of app */}
</>
)
}
7. Server-Side Rendering (Next.js)
// pages/_document.tsx (Pages Router)
import createEmotionServer from '@emotion/server/create-instance'
import createCache from '@emotion/cache'
import Document, { Html, Head, Main, NextScript } from 'next/document'
export default function MyDocument({ emotionStyleTags }) {
return (
<Html>
<Head>{emotionStyleTags}</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
)
}
MyDocument.getInitialProps = async (ctx) => {
const cache = createCache({ key: 'css' })
const { extractCriticalToChunks } = createEmotionServer(cache)
const initialProps = await Document.getInitialProps(ctx)
const emotionStyles = extractCriticalToChunks(initialProps.html)
const emotionStyleTags = emotionStyles.styles.map(style => (
<style
data-emotion={`${style.key} ${style.ids.join(' ')}`}
key={style.key}
dangerouslySetInnerHTML={{ __html: style.css }}
/>
))
return { ...initialProps, emotionStyleTags }
}
Emotion vs Tailwind CSS vs styled-components
| Emotion | Tailwind CSS | styled-components | |
|---|---|---|---|
| Bundle | Small (runtime) | Zero runtime | Similar to Emotion |
| Dynamic styles | Easy | Limited (JIT) | Easy |
| Theming | Built-in | Config-based | Built-in |
| TypeScript | Excellent | Good | Good |
| Learning curve | Low (CSS syntax) | Medium (utility classes) | Low |
| Performance | Good | Best (no runtime) | Good |
| SSR | Supported | No issue | Supported |
| Best for | Design systems, theming | Rapid UI, utility-first | Component libraries |
Key Takeaways
- Two APIs:
styled(component-based) andcssprop (inline, needs pragma) - Dynamic styles: use props in template literals for variant-based styling
- Theming:
ThemeProvider+ typed theme +useThemehook — full autocomplete - Composition: combine
cssfragments with arrays or${fragment}in template literals - SSR: use
@emotion/serverfor Next.js; required for avoiding FOUC - When to use: complex theming, design systems, programmatic styles — use Tailwind for utility-first rapid development