Website Markup Using Emotion CSS-in-JS
Emotion is a CSS-in-JS library with two APIs: @emotion/css for vanilla JS and @emotion/react for React. Used as styling engine in MUI, Chakra UI and several other libraries. Among CSS-in-JS tools shows best performance: supports static extraction via Babel plugin, eliminating runtime overhead for static styles.
Installation
# For React
npm install @emotion/react @emotion/styled
# Optional: Babel plugin for optimization
npm install -D @emotion/babel-plugin
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [
react({
jsxImportSource: '@emotion/react',
babel: {
plugins: ['@emotion/babel-plugin'],
},
}),
],
});
When using jsxImportSource: '@emotion/react' you can use css prop directly without /** @jsxImportSource @emotion/react */ in each file.
Two Approaches: css Prop and styled
css prop — Inline Styles with Full CSS Power
import { css } from '@emotion/react';
// Static block — computed once
const heroStyles = css`
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100svh;
padding: 2rem;
text-align: center;
@media (min-width: 1024px) {
flex-direction: row;
text-align: left;
padding: 4rem;
gap: 5rem;
}
`;
const HeroSection = () => (
<section css={heroStyles}>
<div>
<h1 css={css`
font-size: clamp(1.75rem, 4vw, 3rem);
font-weight: 700;
margin-bottom: 1rem;
line-height: 1.2;
`}>
Page Heading
</h1>
</div>
</section>
);
styled — Component API
import styled from '@emotion/styled';
interface CardProps {
elevated?: boolean;
interactive?: boolean;
}
const Card = styled.article<CardProps>`
background: var(--color-surface);
border-radius: 0.75rem;
padding: 1.5rem;
border: 1px solid var(--color-border);
box-shadow: ${({ elevated }) =>
elevated ? '0 10px 15px -3px rgb(0 0 0 / 0.1)' : '0 1px 2px rgb(0 0 0 / 0.05)'};
${({ interactive }) =>
interactive &&
`
cursor: pointer;
transition: transform 200ms ease, box-shadow 200ms ease;
&:hover {
transform: translateY(-3px);
box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1);
}
`}
`;
const CardTitle = styled.h3`
font-size: 1.125rem;
font-weight: 600;
margin-bottom: 0.5rem;
color: var(--color-text-primary);
`;
const CardBody = styled.p`
font-size: 0.875rem;
color: var(--color-text-secondary);
line-height: 1.6;
`;
Theme via ThemeProvider
// src/theme/emotion-theme.ts
export const theme = {
colors: {
primary: '#2563eb',
primaryDark: '#1d4ed8',
primaryLight: '#eff6ff',
background: '#f9fafb',
surface: '#ffffff',
textPrimary: '#111827',
textSecondary: '#6b7280',
border: '#e5e7eb',
},
space: (n: number) => `${n * 0.25}rem`,
radius: {
sm: '4px',
md: '8px',
lg: '12px',
},
shadow: {
sm: '0 1px 2px rgb(0 0 0 / 0.05)',
md: '0 4px 6px -1px rgb(0 0 0 / 0.1)',
},
} as const;
export type Theme = typeof theme;
// Add to Emotion types
declare module '@emotion/react' {
export interface Theme {
colors: typeof theme.colors;
space: typeof theme.space;
radius: typeof theme.radius;
shadow: typeof theme.shadow;
}
}
// src/App.tsx
import { ThemeProvider, Global, css } from '@emotion/react';
import { theme } from './theme/emotion-theme';
const globalStyles = css`
*, *::before, *::after { box-sizing: border-box; }
body {
font-family: 'Inter', system-ui, sans-serif;
background: ${theme.colors.background};
color: ${theme.colors.textPrimary};
-webkit-font-smoothing: antialiased;
}
`;
const App = () => (
<ThemeProvider theme={theme}>
<Global styles={globalStyles} />
<Router />
</ThemeProvider>
);
Using theme in styled components:
import styled from '@emotion/styled';
const PrimaryButton = styled.button`
background: ${({ theme }) => theme.colors.primary};
color: #fff;
padding: ${({ theme }) => `${theme.space(2)} ${theme.space(4)}`};
border-radius: ${({ theme }) => theme.radius.md};
box-shadow: ${({ theme }) => theme.shadow.sm};
&:hover {
background: ${({ theme }) => theme.colors.primaryDark};
}
`;
keyframes for Animations
import { keyframes } from '@emotion/react';
import styled from '@emotion/styled';
const fadeInUp = keyframes`
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
`;
const AnimatedCard = styled.div<{ delay?: number }>`
animation: ${fadeInUp} 400ms ease both;
animation-delay: ${({ delay = 0 }) => `${delay}ms`};
`;
// Usage
const FeaturesList = ({ features }) => (
<>
{features.map((feature, index) => (
<AnimatedCard key={feature.id} delay={index * 80}>
{/* ... */}
</AnimatedCard>
))}
</>
);
cx() for Conditional Classes
import { css, cx } from '@emotion/css';
const base = css`
padding: 1rem;
border-radius: 8px;
`;
const active = css`
background: #eff6ff;
color: #2563eb;
font-weight: 600;
`;
const MenuItem = ({ label, isActive }) => (
<a className={cx(base, isActive && active)}>
{label}
</a>
);
Comparison with Styled Components
| Emotion | Styled Components | |
|---|---|---|
| Size | ~8 KB | ~15 KB |
| Performance | Higher | Slightly lower |
| SSR | Built-in | Requires ServerStyleSheet |
| css prop | Yes | No (only via babel) |
| Popularity | Growing | Established |
| Babel plugin | Yes | Yes |
Timeframes
Setup Emotion with ThemeProvider and typing: 2–3 hours. Landing page markup: 1–2 days. Emotion is especially good in projects where Styled Components lacks performance or css prop is needed for dynamic styles.







