Вёрстка сайта з використанням Emotion CSS-in-JS
Emotion — CSS-in-JS бібліотека з двома API: @emotion/css для vanilla JS та @emotion/react для React. Використовується як styling engine в MUI, Chakra UI та ряді інших бібліотек. Серед CSS-in-JS інструментів показує найкращу продуктивність: підтримує статичну екстракцію через Babel-плагін, що усуває runtime-overhead для статичних стилів.
Установка
# Для React
npm install @emotion/react @emotion/styled
# Опціонально: Babel-плагін для оптимізації
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'],
},
}),
],
});
При використанні jsxImportSource: '@emotion/react' можна використовувати css prop напрямку без /** @jsxImportSource @emotion/react */ у кожному файлі.
Два підходи: css prop та styled
css prop — інлайн-стилі з повною мощью CSS
import { css } from '@emotion/react';
// Статичний блок — обчислюється один раз
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;
`}>
Заголовок сторінки
</h1>
</div>
</section>
);
styled — компонентний 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;
`;
Тема через 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;
// Додати в типи Emotion
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>
);
Використання теми в styled-компонентах:
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 для анімацій
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`};
`;
// Використання
const FeaturesList = ({ features }) => (
<>
{features.map((feature, index) => (
<AnimatedCard key={feature.id} delay={index * 80}>
{/* ... */}
</AnimatedCard>
))}
</>
);
cx() для умовних класів
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>
);
Порівняння зі Styled Components
| Emotion | Styled Components | |
|---|---|---|
| Розмір | ~8 КБ | ~15 КБ |
| Продуктивність | Вища | Трохи нижча |
| SSR | Вбудована | Потребує ServerStyleSheet |
| css prop | Так | Ні (тільки через babel) |
| Популярність | Растуча | Усталена |
| Babel-плагін | Так | Так |
Терміни
Налаштування Emotion з ThemeProvider та типізацією: 2–3 години. Вёрстка посадочної сторінки: 1–2 дні. Emotion особливо хороший в проектах, де Styled Components не хватає продуктивності або потрібен css prop для динамічних стилів.







