В 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 сам по себе не зло. Проблемы начинаются, когда им стилизуют весь интерфейс без ограничений.

Его хорошее применение в 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 или динамику. Такой порядок обычно дает лучший баланс между скоростью, читаемостью и стоимостью рантайма.