Способы применения CSS, токены и архитектура стилей
На современном фронтенде стили можно писать десятком способов — от архаичного style={} до zero-runtime компиляции. Каждый подход решает свою задачу: инкапсуляцию, динамичность, производительность, DX. Отдельная часть — архитектура стилей на больших проектах: CSS custom properties для темизации, дизайн-токены и методологии именования вроде BEM, CUBE и ITCSS. На собеседовании важно не просто перечислить варианты, а объяснить trade-offs: что каждый подход даёт и что отнимает, как влияет на бандл, SSR и поддерживаемость.
Содержание
- Глубокий разбор
- Custom properties и токены
- Методологии именования
- Сравнительная таблица
- Тренд 2025–2026
- Практические примеры
- Типичные ошибки и антипаттерны
- Ключевые тезисы для интервью
- Заключение
- Полезные ссылки
Глубокий разбор
1. Inline styles
Стили напрямую в атрибуте style элемента.
function Badge({ count }) {
return (
<span style={{
backgroundColor: count > 0 ? 'red' : 'gray',
color: 'white',
borderRadius: 12,
padding: '2px 8px',
fontSize: 12,
}}>
{count}
</span>
);
}
Как работает. Браузер преобразует объект в CSS-строку и записывает в style элемента. Каждый рендер генерирует новый объект — React не может сравнить ссылки и обновляет DOM-атрибут.
Плюсы:
- Нулевая настройка — работает из коробки.
- Полный доступ к JS-переменным и props без посредников.
- Нет проблем с именами классов.
Минусы:
- Нельзя использовать псевдоклассы (
:hover,:focus) и псевдоэлементы (::before). - Нельзя использовать медиа-запросы (без JS-костылей).
- Высокая специфичность inline-атрибута — сложно переопределить.
- Нет кеширования: стили пересоздаются при каждом рендере.
- Префиксы (vendor) не добавляются автоматически.
- Нельзя анимировать через
@keyframes(только через JS). - Увеличение HTML-размера: стили дублируются в каждом элементе.
Когда использовать: Прототипы, единичные динамические значения (позиция tooltip, ширина прогресс-бара), conditional rendering простых стилей. Не подходит для системного стилирования.
2. Глобальный CSS
Классический подход: один или несколько .css-файлов, подключаемых через import './styles.css' или <link>.
/* global.css */
.button {
background: #3b82f6;
color: white;
padding: 8px 16px;
border-radius: 6px;
}
.button:hover {
background: #2563eb;
}
import './global.css';
function Button({ children }) {
return <button className="button">{children}</button>;
}
Как работает. CSS-файл парсится, все правила попадают в единый CSSOM. Классы глобальны — любой элемент с классом .button получит эти стили.
Плюсы:
- Максимальная простота, нет зависимостей.
- Полная мощность CSS: псевдоклассы, медиа-запросы, анимации,
@layer. - Кеширование браузером — файл загружается один раз.
- Минимальный размер бандла — нет runtime-кода.
Минусы:
- Глобальное пространство имён — конфликты классов на крупных проектах.
- Нет tree shaking: весь CSS загружается, даже неиспользуемые классы.
- Сложность отслеживания, какой компонент использует какой класс.
- Удаление класса из компонента не гарантирует, что он не используется где-то ещё.
- Порядок импортов влияет на результат.
Когда использовать: Малые проекты, лендинги, глобальные стили (reset, typography), утилитарные классы. На больших проектах — только в комбинации с методологией (BEM) или @layer.
3. CSS Modules
Файлы .module.css, в которых все имена классов автоматически локализуются.
/* Button.module.css */
.button {
background: #3b82f6;
color: white;
padding: 8px 16px;
border-radius: 6px;
}
.primary {
background: #3b82f6;
}
.secondary {
background: transparent;
border: 1px solid #3b82f6;
color: #3b82f6;
}
.button:hover {
background: #2563eb;
}
import styles from './Button.module.css';
function Button({ variant = 'primary', children }) {
return (
<button className={`${styles.button} ${styles[variant]}`}>
{children}
</button>
);
}
Как работает. Бандлер (Vite, Webpack, Next.js) при сборке переименовывает классы в уникальные идентификаторы вида Button_button__x7K2p. CSS-файл остаётся обычным CSS, генерируется объект-словарь для импорта в JS.
Исходный CSS: .button { background: blue; }
Скомпилированный: .Button_button__x7K2p { background: blue; }
Плюсы:
- Локальность классов — нет глобальных конфликтов.
- Нулевой runtime — на выходе обычный CSS-файл.
- Tree shaking: неиспользуемые классы удаляются бандлером.
- Полная поддержка CSS: псевдоклассы, медиа-запросы, анимации,
@layer. - Кеширование браузером.
- Предсказуемая специфичность — один класс = одна специфичность.
- Отличная поддержка в Next.js, Vite, Create React App.
Минусы:
- Динамические стили через props неудобны — нужны CSS-переменные или условные классы.
- Нет автоматической генерации стилей из props (в отличие от CSS-in-JS).
composesработает, но менее мощён, чем наследование в styled-components.- Имена классов в DevTools нечитаемы (хэши).
Композиция классов:
/* base.module.css */
.base {
padding: 8px 16px;
border-radius: 6px;
font-weight: 500;
}
/* Button.module.css */
.button {
composes: base from './base.module.css';
background: #3b82f6;
color: white;
}
composes — аналог наследования: класс .button получит все свойства .base без дублирования CSS.
Глобальные классы внутри CSS Modules:
/* Button.module.css */
.button { /* локальный */ }
:global(.active) .button { /* глобальный .active */ }
Динамические стили через CSS-переменные:
/* Card.module.css */
.card {
width: var(--card-width, 300px);
background: var(--card-bg, white);
}
function Card({ width, bg, children }) {
return (
<div
className={styles.card}
style={{ '--card-width': `${width}px`, '--card-bg': bg }}
>
{children}
</div>
);
}
Когда использовать: Основной выбор для React/Next.js проектов среднего и крупного размера. Оптимален по соотношению DX, производительности и инкапсуляции.
4. CSS-in-JS (runtime): styled-components, Emotion
Стили описываются как JavaScript-выражения, генерируются и внедряются в DOM в runtime.
styled-components
import styled from 'styled-components';
const Button = styled.button`
background: ${p => p.$primary ? '#3b82f6' : 'transparent'};
color: ${p => p.$primary ? 'white' : '#3b82f6'};
padding: 8px 16px;
border-radius: 6px;
border: 1px solid ${p => p.$primary ? 'transparent' : '#3b82f6'};
&:hover {
background: ${p => p.$primary ? '#2563eb' : 'rgba(59, 130, 246, 0.1)'};
}
@media (min-width: 768px) {
padding: 12px 24px;
}
`;
function App() {
return (
<>
<Button $primary>Primary</Button>
<Button>Secondary</Button>
</>
);
}
Emotion (object syntax)
/** @jsxImportSource @emotion/react */
import { css } from '@emotion/react';
const buttonStyles = (primary) => css`
background: ${primary ? '#3b82f6' : 'transparent'};
color: ${primary ? 'white' : '#3b82f6'};
padding: 8px 16px;
border-radius: 6px;
`;
function Button({ primary, children }) {
return <button css={buttonStyles(primary)}>{children}</button>;
}
Как работает. При первом рендере компонента библиотека:
- Парсит шаблонный литерал (или объект).
- Генерирует уникальное имя класса (хэш).
- Создаёт
<style>элемент в<head>и вставляет CSS-правило. - Назначает класс элементу.
Последующие рендеры с теми же props переиспользуют кешированный класс. Новые комбинации props — новые правила.
Transient props ($-префикс):
В styled-components props с $ не попадают в DOM — это предотвращает предупреждения React о неизвестных атрибутах.
const Box = styled.div`
color: ${p => p.$color};
`;
<Box $color="red" color="blue" />
// $color — только для стилизации, color — DOM-атрибут
Плюсы:
- Полная мощность JS для генерации стилей: условия, циклы, функции.
- Доступ к props, теме, контексту без посредников.
- Автоматическая инкапсуляция — уникальные имена классов.
- Автоматический vendor prefixing (через stylis в Emotion/styled-components).
- Удаление мёртвого CSS — стили удаляются при unmount компонента.
- Темизация из коробки:
<ThemeProvider>. - TypeScript-интеграция: типизация props и темы.
Минусы:
- Runtime-накладные расходы: парсинг, хэширование, DOM-манипуляции при первом рендере.
- Увеличение JS-бандла: ~10-15 KB (styled-components) или ~8 KB (Emotion).
- Дублирование CSS-правил в
<style>при множестве уникальных комбинаций props. - Сложности с SSR: нужно извлечь стили на сервере и передать клиенту (
ServerStyleSheet). - Нет кеширования CSS-файла браузером — стили живут в JS.
- Проблема «specificity war» при микшировании с глобальным CSS.
- Потенциальные проблемы с Content Security Policy (inline
<style>).
Производительность: На небольших проектах незаметна. На больших (100+ компонентов с динамическими стилями) — может быть ощутима при первом рендере (FCP/LCP). Библиотеки кешируют результаты, но первый проход по-прежнему дорог.
Когда использовать: Проекты с высокой динамичностью стилей (theming, user-customizable UI), дизайн-системы с runtime-темизацией, команды, которым важен colocation (стили рядом с компонентом в одном файле).
5. CSS-in-JS (zero-runtime): Vanilla Extract, Linaria, Panda CSS
Стили пишутся в JS/TS, но компилируются в статические CSS-файлы на этапе сборки. Runtime-кода нет.
Vanilla Extract
// Button.css.ts
import { style, styleVariants } from '@vanilla-extract/css';
export const button = style({
padding: '8px 16px',
borderRadius: '6px',
fontWeight: 500,
});
export const variant = styleVariants({
primary: {
background: '#3b82f6',
color: 'white',
},
secondary: {
background: 'transparent',
border: '1px solid #3b82f6',
color: '#3b82f6',
},
});
// Button.tsx
import * as styles from './Button.css';
interface ButtonProps {
variant?: keyof typeof styles.variant;
children: React.ReactNode;
}
export function Button({ variant = 'primary', children }: ButtonProps) {
return (
<button className={`${styles.button} ${styles.variant[variant]}`}>
{children}
</button>
);
}
Как работает. Плагин для бандлера (Vite, Webpack, Next.js) на этапе сборки:
- Находит
.css.tsфайлы. - Выполняет их в Node.js (не в браузере).
- Генерирует статические
.cssфайлы с хэшированными именами классов. - Заменяет экспорты в JS на строковые идентификаторы классов.
На выходе — обычный CSS-файл и JS без runtime-кода для стилей.
Динамические стили через CSS-переменные:
// Card.css.ts
import { style } from '@vanilla-extract/css';
export const card = style({
width: 'var(--card-width)',
background: 'var(--card-bg)',
});
function Card({ width, bg, children }) {
return (
<div
className={styles.card}
style={{ '--card-width': `${width}px`, '--card-bg': bg }}
>
{children}
</div>
);
}
Linaria
import { styled } from 'linaria/react';
import { css } from 'linaria';
const Button = styled.button<{ $primary?: boolean }>`
background: ${p => p.$primary ? '#3b82f6' : 'transparent'};
color: ${p => p.$primary ? 'white' : '#3b82f6'};
padding: 8px 16px;
border-radius: 6px;
`;
Linaria анализирует шаблонные литералы на этапе сборки и извлекает статические CSS-правила. Динамические значения через props компилируются в CSS-переменные.
Panda CSS
// panda.config.ts
import { defineConfig } from '@pandacss/dev';
export default defineConfig({
theme: {
extend: {
tokens: {
colors: {
primary: { value: '#3b82f6' },
},
},
},
},
});
import { css } from '../styled-system/css';
function Button({ primary, children }) {
return (
<button className={css({
bg: primary ? 'primary' : 'transparent',
color: primary ? 'white' : 'primary',
px: 4,
py: 2,
rounded: 'md',
})}>
{children}
</button>
);
}
Panda CSS генерирует утилитарные CSS-классы на этапе сборки — подход, сочетающий DX CSS-in-JS с производительностью utility-first.
Плюсы:
- Нулевой runtime — на выходе статический CSS.
- Полная типизация (TypeScript-first).
- Инкапсуляция через хэшированные имена.
- Tree shaking: неиспользуемые стили не попадают в бандл.
- Кеширование CSS-файла браузером.
- Нет проблем с SSR — обычный CSS.
- Нет проблем с CSP — нет inline
<style>.
Минусы:
- Динамические стили ограничены CSS-переменными (нельзя генерировать новые классы в runtime).
- Более сложная настройка (плагины для бандлера).
- Меньше библиотек и экосистемы по сравнению с runtime-решениями.
- Долгий initial build из-за компиляции.
Когда использовать: Когда нужен DX CSS-in-JS, но критична производительность. Оптимальный выбор для новых проектов на Next.js/Vite.
6. Utility-first CSS: Tailwind CSS
Стили собираются из атомарных утилитарных классов прямо в JSX.
function Card({ title, description }) {
return (
<div className="rounded-lg bg-white p-6 shadow-md hover:shadow-lg transition-shadow">
<h2 className="text-xl font-bold text-gray-900">{title}</h2>
<p className="mt-2 text-gray-600">{description}</p>
</div>
);
}
Как работает. Tailwind сканирует исходники, находит используемые утилиты и генерирует минимальный CSS-файл только с нужными классами. На этапе сборки — не runtime.
/* Сгенерированный output.css */
.rounded-lg { border-radius: 0.5rem; }
.bg-white { background-color: #fff; }
.p-6 { padding: 1.5rem; }
.shadow-md { box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1); }
/* ... только используемые классы */
Плюсы:
- Минимальный CSS-бандл — только используемые утилиты.
- Нет придумывания имён классов.
- Быстрый прототипинг — стили не покидают JSX.
- Консистентность: ограничения дизайн-системы в конфиге (цвета, отступы, шрифты).
- Responsive и state-модификаторы прямо в классе:
md:flex,hover:bg-blue-700. - Тёмная тема:
dark:bg-gray-900. - Нет runtime — чистый CSS на выходе.
Минусы:
- Длинные className — ухудшение читаемости JSX.
- Нельзя выразить сложные стили (многослойные тени, кастомные анимации) без
@applyили arbitrary values. - CSS-файл всё ещё глобален — нет инкапсуляции на уровне компонента.
- Переключение контекста между JSX и CSS-мышлением.
- Legacy-код с Tailwind тяжело рефакторить.
Arbitrary values:
<div className="w-[350px] bg-[#1da1f2] grid-cols-[repeat(3,minmax(0,1fr))]">
Компонентный подход с cva (class-variance-authority):
import { cva } from 'class-variance-authority';
import { cn } from './utils';
const button = cva(
'rounded-md font-bold transition-colors',
{
variants: {
variant: {
primary: 'bg-blue-500 text-white hover:bg-blue-600',
secondary: 'border border-blue-500 text-blue-500 hover:bg-blue-50',
ghost: 'text-blue-500 hover:bg-blue-50',
},
size: {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-base',
lg: 'px-6 py-3 text-lg',
},
},
defaultVariants: {
variant: 'primary',
size: 'md',
},
}
);
function Button({ variant, size, className, children }) {
return (
<button className={cn(button({ variant, size }), className)}>
{children}
</button>
);
}
Когда использовать: Команды, ценящие скорость разработки и консистентность; дизайн-системы с чёткими токенами; проекты, где важна минимальность CSS-бандла.
7. CSS @scope
Новый CSS-механизм для ограничения области видимости стилей без CSS Modules.
@scope (.card) to (.card__content) {
:scope {
padding: 16px;
background: white;
}
a {
color: blue;
}
img {
border-radius: 8px;
}
}
Как работает. @scope определяет корень (.card) и границу (.card__content). Стили применяются только к элементам внутри этой области, не выходя за границу.
Плюсы:
- Нативный CSS — нет зависимостей и сборки.
- Инкапсуляция без хэширования имён.
- Можно применять к существующему CSS без миграции.
Минусы:
- Ограниченная поддержка браузеров (на 2026 — Chromium, частично Firefox).
- Не решает проблему именования — классы всё ещё глобальны вне scope.
- Не интегрируется с JS-модулями напрямую.
Когда использовать: Прогрессивное улучшение, виджеты на чистом HTML/CSS, изоляция legacy-стилей.
8. Shadow DOM и стили в Web Components
Shadow DOM обеспечивает полную инкапсуляцию: стили внутри не влияют на внешний мир и наоборот.
class MyButton extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = `
<style>
button {
background: #3b82f6;
color: white;
padding: 8px 16px;
border-radius: 6px;
}
button:hover {
background: #2563eb;
}
</style>
<button><slot></slot></button>
`;
}
}
customElements.define('my-button', MyButton);
Плюсы:
- Полная изоляция — стили не протекают ни в одну сторону.
- Идеально для переиспользуемых виджетов.
- Нативный браузерный механизм.
Минусы:
- Нельзя стилизовать извне (кроме custom properties, которые проникают через Shadow DOM).
- Сложности с глобальными темами и дизайн-системами.
- Не все CSS-фреймворки работают с Shadow DOM.
Когда использовать: Web Components, виджеты для сторонних сайтов, микрофронтенды.
Custom properties и токены
CSS custom properties
Custom properties (пользовательские свойства, часто называемые CSS-переменными) — это именованные значения, объявляемые с префиксом -- и используемые через var().
:root {
--color-primary: #3b82f6;
--space-md: 1rem;
--radius-base: 0.5rem;
}
.button {
background: var(--color-primary);
padding: var(--space-md);
border-radius: var(--radius-base);
}
В отличие от препроцессорных переменных (Sass, Less), custom properties живут в runtime браузера: их можно переопределять в медиа-запросах, псевдоклассах, через JavaScript и наследовать по DOM.
Область видимости и наследование
Custom properties наследуются, как и большинство CSS-свойств. Область видимости определяется селектором, в котором они объявлены.
:root {
--accent: blue;
}
.card {
--accent: green;
}
.card .title {
color: var(--accent); /* green, если .title внутри .card */
}
Это позволяет создавать локальные переопределения: один и тот же компонент выглядит по-разному в разных контекстах без изменения HTML или классов.
Резервные значения
var() поддерживает второй аргумент — значение по умолчанию, которое используется, если переменная не определена.
.button {
color: var(--button-color, white);
}
Важный нюанс: резервное значение подставляется только если переменная не задана. Если переменная задана, но содержит невалидное для свойства значение, резервное не сработает — свойство получит initial value.
Типизация и вычисления
CSS custom properties — это строки. Браузер не знает, что внутри --size: 16px, пока не подставит значение в свойство. Поэтому нельзя просто сложить var(--a) + var(--b); для вычислений используется calc().
:root {
--base: 1rem;
--scale: 2;
}
.box {
padding: calc(var(--base) * var(--scale));
}
Если переменная используется в месте, где ожидается число без единиц, применяют трюк с умножением на единицу: calc(var(--value) * 1px).
Темизация и дизайн-токены
Custom properties — стандартный способ реализации design tokens: именованных значений цветов, отступов, типографики, теней.
:root {
--color-bg: #ffffff;
--color-text: #111827;
--color-border: #e5e7eb;
--space-xs: 0.25rem;
--space-sm: 0.5rem;
--space-md: 1rem;
--font-base: system-ui, sans-serif;
}
@media (prefers-color-scheme: dark) {
:root {
--color-bg: #111827;
--color-text: #f9fafb;
--color-border: #374151;
}
}
Токены обычно делят на уровни:
- Primitive tokens — низкоуровневые значения:
--blue-500,--space-4. - Semantic tokens — значения смысла:
--color-surface,--color-text-default. - Component tokens — значения для конкретного компонента:
--button-bg,--input-border.
Семантический слой позволяет менять тему, не трогая примитивы: достаточно переназначить --color-surface в зависимости от контекста.
Динамические стили через CSS-переменные
Статический класс ссылается на переменные, а JS задаёт только их значения:
/* Card.module.css */
.card {
width: var(--card-width, 300px);
background: var(--card-bg, white);
}
function Card({ width, bg, children }) {
return (
<div
className={styles.card}
style={{ '--card-width': `${width}px`, '--card-bg': bg }}
>
{children}
</div>
);
}
Новые значения применяются без генерации новых CSS-правил, сохраняя кеширование и работая в SSR/RSC без runtime.
Методологии именования
BEM
BEM (Block, Element, Modifier) — классическая методология для именования классов:
<button class="button button--primary button--large">Save</button>
.button { }
.button__icon { }
.button--primary { }
.button--large { }
BEM решает проблему специфичности и делает структуру классов предсказуемой. Недостаток: длинные имена и жёсткая привязка к компоненту.
CUBE CSS
CUBE CSS — подход, в котором стили разделяются на слои:
- Composition — раскладка и пространство (flex/grid-утилиты, контейнеры).
- Utility — мелкие одноцелевые классы.
- Block — компоненты в духе BEM.
- Exception — состояния и модификаторы, часто через
data-*атрибуты.
<div class="cluster | card" data-state="highlighted">
CUBE акцентирует внимание на глобальных утилитах и раскладке, а компоненты оставляет минималистичными.
ITCSS
ITCSS (Inverted Triangle CSS) — архитектурная система, которая организует стили от глобальных к локальным:
- Settings — переменные и конфигурация.
- Tools — миксины и функции.
- Generic — сбросы и нормализация.
- Elements — стили тегов.
- Objects — раскладочные паттерны.
- Components — компоненты.
- Utilities — вспомогательные классы.
- Trumps — переопределения с высшим приоритетом.
ITCSS хорошо сочетается с @layer: каждый уровень треугольника может быть отдельным слоем.
@layer в архитектуре
CSS Layers позволяют явно управлять приоритетом стилей, не повышая специфичность. Это особенно полезно в архитектуре, где есть reset, base, components и utilities.
@layer reset, base, components, utilities;
@layer reset {
*, *::before, *::after { box-sizing: border-box; }
}
@layer components {
.button { background: blue; }
}
@layer utilities {
.bg-red { background: red !important; }
}
Слои объявлены один раз, и их порядок определяет приоритет. Правила вне @layer имеют наивысший приоритет среди author-стилей. Без слоёв утилиты и компоненты начинают бороться через !important и высокую специфичность.
Сравнительная таблица
| Подход | Runtime | Инкапсуляция | Динамические стили | CSS-бандл | SSR | Типизация |
|---|---|---|---|---|---|---|
| Inline | Нет | Полная | Да | Нет CSS | Да | Частичная |
| Глобальный CSS | Нет | Нет | Через CSS-переменные | Полный | Да | Нет |
| CSS Modules | Нет | Локальные классы | Через CSS-переменные | Tree-shaken | Да | Через .d.ts |
| CSS-in-JS (runtime) | Да | Хэш-классы | Полная | Нет CSS | Сложнее | Да |
| CSS-in-JS (zero-runtime) | Нет | Хэш-классы | CSS-переменные | Tree-shaken | Да | Да |
| Tailwind CSS | Нет | Нет (утилиты) | Через arbitrary values | Минимальный | Да | Через плагины |
@scope | Нет | Через scope | Через CSS-переменные | Полный | Да | Нет |
| Shadow DOM | Нет | Полная | Да | Нет CSS | Да | Нет |
Тренд 2025–2026
Индустрия движется от runtime CSS-in-JS к compile-time решениям. Причины:
- Performance. Runtime CSS-in-JS добавляет задержку к FCP/LCP. На мобильных устройствах с низким CPU это заметно.
- React Server Components. Серверные компоненты не могут использовать runtime CSS-in-JS — нет JS-рантайма на клиенте для генерации стилей. Zero-runtime решения и CSS Modules работают с RSC.
- Bundle size. Каждый KB JS-бандла — это время загрузки и парсинга. CSS-файл кешируется отдельно и не блокирует JS-выполнение.
- CSP. Политики безопасности всё чаще запрещают inline
<style>, что ломает runtime CSS-in-JS.
Рекомендуемый стек для нового React/Next.js проекта:
- CSS Modules — для компонентных стилей (простота, нулевой runtime).
- CSS-переменные — для токенов и темизации.
@layer— для управления каскадом.- Tailwind CSS — для утилитарных стилей и быстрого прототипирования.
- Vanilla Extract / Panda CSS — если нужен типизированный CSS-in-JS DX без runtime.
Практические примеры
Пример 1: CSS Modules + темизация через CSS-переменные
/* Button.module.css */
.button {
--btn-bg: var(--color-primary);
--btn-color: var(--color-text-inverted);
--btn-border: transparent;
background: var(--btn-bg);
color: var(--btn-color);
border: 1px solid var(--btn-border);
padding: var(--space-2) var(--space-4);
border-radius: var(--radius-md);
font-weight: 500;
cursor: pointer;
transition: background 0.15s ease;
}
.button:hover {
--btn-bg: var(--color-primary-hover);
}
.outline {
--btn-bg: transparent;
--btn-color: var(--color-primary);
--btn-border: var(--color-primary);
}
.ghost {
--btn-bg: transparent;
--btn-color: var(--color-primary);
--btn-border: transparent;
}
.ghost:hover {
--btn-bg: var(--color-primary-alpha-10);
}
.disabled {
opacity: 0.5;
cursor: not-allowed;
pointer-events: none;
}
import styles from './Button.module.css';
import { cn } from './utils';
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'solid' | 'outline' | 'ghost';
}
export function Button({ variant = 'solid', className, ...props }: ButtonProps) {
return (
<button
className={cn(
styles.button,
variant === 'outline' && styles.outline,
variant === 'ghost' && styles.ghost,
props.disabled && styles.disabled,
className
)}
{...props}
/>
);
}
Пример 2: Vanilla Extract с рецептами (sprinkles)
// sprinkles.css.ts
import { defineProperties, createSprinkles } from '@vanilla-extract/sprinkles';
const responsiveProperties = defineProperties({
conditions: {
mobile: {},
tablet: { '@media': '(min-width: 768px)' },
desktop: { '@media': '(min-width: 1024px)' },
},
defaultCondition: 'mobile',
properties: {
display: ['none', 'flex', 'block', 'grid'],
flexDirection: ['row', 'column'],
padding: { small: '8px', medium: '16px', large: '24px' },
gap: { small: '8px', medium: '16px', large: '24px' },
},
shorthands: {
p: ['padding'],
fd: ['flexDirection'],
},
});
const colorProperties = defineProperties({
properties: {
background: {
primary: '#3b82f6',
surface: '#ffffff',
danger: '#ef4444',
},
color: {
default: '#111827',
inverted: '#ffffff',
muted: '#6b7280',
},
},
});
export const sprinkles = createSprinkles(responsiveProperties, colorProperties);
import { sprinkles } from './sprinkles.css';
function Card() {
return (
<div className={sprinkles({
display: 'flex',
fd: 'column',
p: 'medium',
background: 'surface',
color: 'default',
gap: { mobile: 'small', desktop: 'medium' },
})}>
<h2>Card title</h2>
<p>Card content</p>
</div>
);
}
Пример 3: Tailwind + cva — компонентный подход
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from './utils';
const badge = cva(
'inline-flex items-center rounded-full font-bold',
{
variants: {
variant: {
success: 'bg-green-100 text-green-800',
warning: 'bg-yellow-100 text-yellow-800',
error: 'bg-red-100 text-red-800',
info: 'bg-blue-100 text-blue-800',
},
size: {
sm: 'px-2 py-0.5 text-xs',
md: 'px-2.5 py-0.5 text-sm',
lg: 'px-3 py-1 text-base',
},
},
defaultVariants: {
variant: 'info',
size: 'md',
},
}
);
type BadgeProps = VariantProps<typeof badge> & {
children: React.ReactNode;
};
function Badge({ variant, size, children }: BadgeProps) {
return <span className={badge({ variant, size })}>{children}</span>;
}
Пример 4: Миграция с runtime CSS-in-JS на CSS Modules
До (styled-components):
const Card = styled.div<{ $highlighted?: boolean }>`
padding: 16px;
border-radius: 8px;
background: ${p => p.$highlighted ? '#eff6ff' : '#ffffff'};
border: 1px solid ${p => p.$highlighted ? '#3b82f6' : '#e5e7eb'};
box-shadow: 0 1px 3px rgb(0 0 0 / 0.1);
`;
После (CSS Modules):
/* Card.module.css */
.card {
padding: 16px;
border-radius: 8px;
background: var(--card-bg, #ffffff);
border: 1px solid var(--card-border, #e5e7eb);
box-shadow: 0 1px 3px rgb(0 0 0 / 0.1);
}
.highlighted {
--card-bg: #eff6ff;
--card-border: #3b82f6;
}
import styles from './Card.module.css';
import { cn } from './utils';
function Card({ highlighted, children }) {
return (
<div className={cn(styles.card, highlighted && styles.highlighted)}>
{children}
</div>
);
}
Пример 5: @layer с ITCSS
@layer settings, generic, elements, objects, components, utilities;
@layer settings {
:root {
--color-primary: #3b82f6;
--space-2: 0.5rem;
--space-4: 1.5rem;
--radius-md: 0.5rem;
}
}
@layer generic {
*, *::before, *::after { box-sizing: border-box; }
}
@layer components {
.button { background: var(--color-primary); }
}
@layer utilities {
.hidden { display: none !important; }
}
Каждый уровень ITCSS становится отдельным слоем: утилиты перекрывают компоненты, а настройки (токены) — самый низкий приоритет.
Типичные ошибки и антипаттерны
- Inline styles для всего. Потеря псевдоклассов, медиа-запросов, кеширования. Inline — только для truly динамических значений.
- Смешение подходов без стратегии. CSS Modules + styled-components + Tailwind в одном проекте — хаос. Выберите один основной подход.
- Динамические классы через интерполяцию в CSS Modules.
styles[\btn-${variant}`]` — хрупкий и непроверяемый. Используйте явные маппинги. - Глобальный CSS без
@layerили методологии. На проекте с 50+ компонентами без системы именования — неизбежные конфликты. - Runtime CSS-in-JS для SSR-проектов без настройки. Забытый
ServerStyleSheet= стили не попадают в первый HTML. - Tailwind без
cva/tv. ДлинныйclassNameс условной логикой в JSX — нечитаем. Вынесите варианты вcva. - Хранение стилей в JS-объектах.
{ color: 'red' }вместо CSS — потеря кеширования, нет псевдоклассов, нет DevTools. - Игнорирование CSS-переменных для динамических значений. Вместо
style={{ width: value }}—style={{ '--width': value }}+width: var(--width)в CSS. - Хранение в custom properties значений, которые нельзя интерполировать. Например,
--radius: 4без единицы требуетcalc(var(--radius) * 1px). - Путаница наследования и каскада. Custom properties наследуются, но переопределение в дочернем элементе не влияет на родителя.
- Смешение примитивных и семантических токенов в одном слое. Примитивы должны быть стабильными, семантические — адаптироваться под тему.
- Переусложнённый BEM:
header__nav__list__item__link. Вложенность элементов в BEM не отражается в имени; достаточноheader__link. - Дублирование токенов в JS и CSS. Если цвета хранятся и в теме styled-components, и в CSS-переменных, появляется риск рассинхронизации.
Ключевые тезисы для интервью
- Inline styles дают полный доступ к JS, но не поддерживают псевдоклассы, медиа-запросы и кеширование — только для единичных динамических значений. Глобальный CSS прост и быстр, но требует дисциплины (BEM,
@layer) на крупных проектах. - CSS Modules создают локальные классы через хэширование с нулевым runtime и полной поддержкой CSS — оптимальный выбор для React/Next.js.
- Runtime CSS-in-JS (styled-components, Emotion) даёт максимальную гибкость и DX, но создаёт runtime-накладные расходы, увеличивает JS-бандл и усложняет SSR и CSP; zero-runtime (Vanilla Extract, Panda CSS) компилирует стили на этапе сборки в чистый CSS, сохраняя DX.
- Tailwind CSS — utility-first подход: на сборке генерирует минимальный CSS только из используемых утилит, ускоряет разработку и убирает придумывание имён; длинные className лечатся через
cva. - Shadow DOM даёт полную изоляцию стилей для Web Components (наружу проникают только custom properties), а
@scopeограничивает область действия правил без хэширования — нативная инкапсуляция без сборки, но с ограниченной поддержкой браузеров. - Custom properties живут в runtime браузера, наследуются по DOM и поддерживают области видимости через селекторы — в отличие от переменных препроцессоров, которые компилируются в статические значения.
var(--name, fallback)не сработает при невалидном значении, а вычисления требуютcalc(). - Токены делятся на три уровня: примитивные (цвета, размеры), семантические (ролевые: primary, danger, text) и компонентные (button-bg, card-border) — семантический слой позволяет менять тему, не трогая примитивы. Методологии BEM/CUBE/ITCSS и
@layerупорядочивают каскад на больших проектах; тренд 2025–2026 — переход от runtime CSS-in-JS к compile-time решениям (CSS Modules + CSS-переменные +@layer+ Tailwind).
Заключение
Выбор подхода к стилизации — это компромисс между динамичностью, производительностью, инкапсуляцией и DX. Инструменты без runtime-кода (CSS Modules, Vanilla Extract, Tailwind) оптимальны для SSR-проектов и React Server Components. Runtime CSS-in-JS сохраняет ценность для сложной темизации и дизайн-систем с высокой динамикой стилей. CSS-переменные — универсальный мост между статическими стилями и динамическими значениями из JS, а токены и методологии (BEM, CUBE, ITCSS) с @layer решают задачу масштабируемости на больших проектах. Понимание trade-offs позволяет выбрать правильный инструмент, а не следовать трендам вслепую.