В React нет одной “правильной” системы стилизации. Из-за этого команды часто выбирают подход по инерции: кто-то тянет глобальный CSS, кто-то сразу прыгает в runtime CSS-in-JS, кто-то смешивает Tailwind, styled-components и обычные .css файлы в одном приложении. Самая дорогая проблема здесь не в технологии, а в хаосе решений.
Хороший выбор зависит не от моды, а от типа продукта. Для обычного продуктового интерфейса важны скорость разработки, предсказуемость каскада, стоимость рантайма и удобство поддержки командой. Поэтому полезно смотреть на стили не как на идеологию, а как на набор инструментов под разные ограничения.
Ниже разберем, где уместны global CSS, CSS Modules, utility-first подход, runtime CSS-in-JS и статически извлекаемый CSS-in-JS, а в конце сведем это в простое правило выбора.
- Global CSS: только для основы
- CSS Modules: хороший дефолт для приложений
- Utility-first CSS: когда нужна скорость и единый дизайн-язык
- Runtime CSS-in-JS: когда динамика действительно важна
- Статически извлекаемый CSS-in-JS
- Практическое правило выбора
- Итог
Global CSS: только для основы
Глобальный CSS сам по себе не зло. Проблемы начинаются, когда им стилизуют весь интерфейс без ограничений.
Его хорошее применение в React-приложении:
- reset или normalize;
- базовая типографика;
- CSS-переменные с дизайн-токенами;
- действительно глобальные layout-правила.
import "./globals.css";
export function App() {
return <div className="app-shell">...</div>;
}Если глобальный CSS начинает описывать конкретные компоненты и экраны, каскад быстро выходит из-под контроля. Поэтому для локальной стилизации компонентов лучше сразу брать более изолированный подход.
CSS Modules: хороший дефолт для приложений
Для большинства продуктовых React-приложений CSS Modules - очень хороший базовый выбор. Они оставляют тебе обычный CSS, но убирают глобальные конфликты имен.
/* Button.module.css */
.root {
padding: 0.75rem 1rem;
border: 0;
border-radius: 0.5rem;
background: #0f172a;
color: #fff;
}
.danger {
background: #b91c1c;
}import styles from "./Button.module.css";
type ButtonProps = {
danger?: boolean;
children: React.ReactNode;
};
export function Button({ danger = false, children }: ButtonProps) {
const className = danger ? `${styles.root} ${styles.danger}` : styles.root;
return <button className={className}>{children}</button>;
}Плюсы CSS Modules:
- привычный CSS без runtime-стоимости;
- локальная область видимости;
- хороший баланс между простотой и масштабируемостью.
Если у команды нет сильной причины идти в другую сторону, CSS Modules часто оказываются самым спокойным дефолтом.
Utility-first CSS: когда нужна скорость и единый дизайн-язык
Utility-first подход вроде Tailwind особенно хорош, когда проекту нужен быстрый темп и жестко ограниченный визуальный язык.
export function ProfileCard() {
return (
<article className="rounded-xl border border-slate-200 bg-white p-6 shadow-sm">
<h2 className="text-lg font-semibold text-slate-900">Alex Myzgin</h2>
<p className="mt-2 text-sm text-slate-600">Frontend engineer</p>
</article>
);
}Сильная сторона utility-first CSS - скорость и предсказуемость. Слабая - длинные className и зависимость от дизайн-системного словаря. Поэтому этот подход особенно хорошо работает, когда команда уже приняла его как единый стиль, а не смешивает с тремя другими системами в каждом новом компоненте.
Runtime CSS-in-JS: когда динамика действительно важна
styled-components и Emotion полезны не потому, что “пишешь CSS в JS”. Их ценность в другом:
- стили живут рядом с компонентом;
- тема и динамические props выражаются естественно;
- удобно строить UI-библиотеки и композиционный API.
import styled from "styled-components";
const Button = styled.button<{ $danger?: boolean }>`
padding: 0.75rem 1rem;
border: 0;
border-radius: 0.5rem;
background: ${({ $danger }) => ($danger ? "#b91c1c" : "#0f172a")};
color: #fff;
`;
export function Example() {
return <Button $danger>Delete</Button>;
}Но runtime CSS-in-JS не стоит брать “по умолчанию” для любого приложения. У него есть цена:
- дополнительные вычисления в рантайме;
- более дорогой рендер на клиенте;
- необходимость аккуратной SSR-настройки, если рендер идет на сервере.
Поэтому такой подход лучше выбирать, когда динамическая стилизация действительно центральна для продукта или библиотеки компонентов.
Статически извлекаемый CSS-in-JS
Если тебе нравится colocated API, но не хочется платить runtime-стоимость за генерацию стилей, смотри в сторону статически извлекаемых решений.
Идея простая: авторский опыт напоминает CSS-in-JS, но итоговый CSS извлекается на этапе сборки.
import { style } from "@vanilla-extract/css";
export const card = style({
padding: "24px",
borderRadius: "16px",
background: "#ffffff",
boxShadow: "0 10px 30px rgba(15, 23, 42, 0.08)",
});import * as styles from "./Card.css";
export function Card({ children }: { children: React.ReactNode }) {
return <section className={styles.card}>{children}</section>;
}Такой подход особенно хорош, когда ты строишь дизайн-систему, хочешь сильную типизацию токенов и не хочешь тянуть дополнительную runtime-стоимость ради самих стилей.
Практическое правило выбора
Если упростить до одного decision tree, он выглядит так:
- нужен только reset, токены и базовая типографика -> global CSS;
- нужен спокойный дефолт для обычного приложения -> CSS Modules;
- нужен быстрый системный UI и команда уже живет в utility-классах -> utility-first CSS;
- нужна сильная динамика, темы и компонентный API -> runtime CSS-in-JS;
- нужен colocated API без runtime-стоимости -> statically extracted CSS-in-JS.
Самая частая ошибка не в том, что команда выбрала “не тот” инструмент, а в том, что она не ограничила область его применения.
Итог
Стилизация React-приложения становится сложной не из-за количества доступных инструментов, а из-за отсутствия четкого правила выбора. Когда у команды есть понятный дефолт и ясные исключения, поддержка интерфейса становится намного спокойнее.
Если нужен практический совет без лишней идеологии, начни с CSS Modules или utility-first подхода, а CSS-in-JS подключай только там, где он действительно покупает тебе удобство API или динамику. Такой порядок обычно дает лучший баланс между скоростью, читаемостью и стоимостью рантайма.