Реализация CSS-in-JS решения для компонентов
CSS-in-JS — подход, при котором стили определяются непосредственно в JavaScript/TypeScript коде компонента. Стили изолированы, типизированы, могут использовать props и переменные без CSS-переменных. За это платят либо runtime-затратами (styled-components, Emotion), либо необходимостью настройки сборки (vanilla-extract, Linaria).
Выбор библиотеки
| Библиотека | Runtime | SSR | Bundle | Когда выбирать |
|---|---|---|---|---|
| Emotion | Да | Да | ~8KB | React-проекты, нужна динамика |
| styled-components | Да | Да | ~13KB | Классика, большая экосистема |
| vanilla-extract | Нет | Да | 0 | Статические стили, максимум производительности |
| Linaria | Нет | Да | 0 | Babel/Vite, статика с интерполяцией |
| Panda CSS | Нет | Да | 0 | Atomic, design tokens, современный проект |
Для нового React-проекта — vanilla-extract (zero-runtime) или Emotion (если нужны динамические стили через props).
Emotion: базовая настройка
npm install @emotion/react @emotion/styled
npm install -D @emotion/babel-plugin # для оптимизации
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [
react({
babel: {
plugins: ['@emotion/babel-plugin'],
},
}),
],
})
Styled Components с Emotion
import styled from '@emotion/styled'
import { css } from '@emotion/react'
// Базовый styled component
const Button = styled.button<{
variant?: 'primary' | 'secondary' | 'ghost'
size?: 'sm' | 'md' | 'lg'
}>`
display: inline-flex;
align-items: center;
gap: 8px;
border-radius: ${({ theme }) => theme.radii.md};
font-weight: 500;
cursor: pointer;
transition: background-color 150ms ease, transform 100ms ease;
&:active { transform: scale(0.98); }
&:disabled { opacity: 0.5; cursor: not-allowed; }
${({ size = 'md' }) =>
({
sm: css`padding: 6px 12px; font-size: 13px;`,
md: css`padding: 10px 20px; font-size: 14px;`,
lg: css`padding: 14px 28px; font-size: 16px;`,
}[size])}
${({ variant = 'primary', theme }) =>
({
primary: css`
background: ${theme.colors.primary};
color: white;
border: none;
&:hover { background: ${theme.colors.primaryHover}; }
`,
secondary: css`
background: transparent;
color: ${theme.colors.primary};
border: 2px solid ${theme.colors.primary};
&:hover { background: ${theme.colors.primaryLight}; }
`,
ghost: css`
background: transparent;
color: ${theme.colors.text};
border: none;
&:hover { background: ${theme.colors.bgHover}; }
`,
}[variant])}
`
css prop
import { css } from '@emotion/react'
function Card({ featured }: { featured?: boolean }) {
return (
<div
css={css`
background: ${featured ? '#eff6ff' : '#fff'};
border: 1px solid ${featured ? '#bfdbfe' : '#e2e8f0'};
border-radius: 12px;
padding: 24px;
box-shadow: ${featured ? '0 4px 12px rgb(37 99 235 / 0.15)' : '0 1px 3px rgb(0 0 0 / 0.08)'};
transition: box-shadow 200ms ease;
&:hover {
box-shadow: 0 8px 24px rgb(0 0 0 / 0.12);
}
`}
>
{/* содержимое */}
</div>
)
}
vanilla-extract: нулевой runtime
npm install @vanilla-extract/css
npm install -D @vanilla-extract/vite-plugin
// vite.config.ts
import { vanillaExtractPlugin } from '@vanilla-extract/vite-plugin'
export default defineConfig({
plugins: [react(), vanillaExtractPlugin()],
})
// button.css.ts — статические стили, генерируются в CSS при сборке
import { styleVariants, style } from '@vanilla-extract/css'
export const base = style({
display: 'inline-flex',
alignItems: 'center',
gap: '8px',
borderRadius: '8px',
fontWeight: 500,
cursor: 'pointer',
transition: 'background-color 150ms ease',
':disabled': {
opacity: 0.5,
cursor: 'not-allowed',
},
})
export const variants = styleVariants({
primary: {
background: '#2563eb',
color: 'white',
border: 'none',
':hover': { background: '#1d4ed8' },
},
secondary: {
background: 'transparent',
color: '#2563eb',
border: '2px solid #2563eb',
},
ghost: {
background: 'transparent',
color: '#0f172a',
border: 'none',
':hover': { background: '#f8fafc' },
},
})
export const sizes = styleVariants({
sm: { padding: '6px 12px', fontSize: '13px' },
md: { padding: '10px 20px', fontSize: '14px' },
lg: { padding: '14px 28px', fontSize: '16px' },
})
// Button.tsx
import { base, variants, sizes } from './button.css'
import clsx from 'clsx'
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: keyof typeof variants
size?: keyof typeof sizes
}
export function Button({ variant = 'primary', size = 'md', className, ...props }: ButtonProps) {
return (
<button
className={clsx(base, variants[variant], sizes[size], className)}
{...props}
/>
)
}
Тема и design tokens в vanilla-extract
// theme.css.ts
import { createTheme, createThemeContract } from '@vanilla-extract/css'
// Контракт — описывает форму темы
export const vars = createThemeContract({
color: {
primary: null,
bg: null,
text: null,
},
radius: {
md: null,
},
})
// Конкретные темы
export const lightTheme = createTheme(vars, {
color: { primary: '#2563eb', bg: '#ffffff', text: '#0f172a' },
radius: { md: '8px' },
})
export const darkTheme = createTheme(vars, {
color: { primary: '#3b82f6', bg: '#0f172a', text: '#f1f5f9' },
radius: { md: '8px' },
})
// Применение темы
import { lightTheme, darkTheme } from './theme.css'
function App({ isDark }: { isDark: boolean }) {
return (
<div className={isDark ? darkTheme : lightTheme}>
{/* все дочерние компоненты используют vars */}
</div>
)
}
Что входит в работу
Выбор подхода под проект (runtime vs zero-runtime), настройка Vite-плагинов, реализация базовых styled компонентов, система тем через ThemeProvider или createTheme, типизация props, интеграция с дизайн-системой проекта.
Срок: 1 день на настройку и перевод существующих компонентов на CSS-in-JS. Больше — при большой кодовой базе.







