CSS Modules в SPA

CSS Modules — система локального скопирования CSS-классов: сборщик автоматически генерирует уникальные имена классов, устраняя конфликты стилей между компонентами.

Зачем нужно

В крупном SPA глобальные CSS-классы вступают в конфликт — .button в одном компоненте перекрывает .button в другом. CSS Modules решают эту проблему на уровне инструментария: разработчик пишет обычный CSS, а сборщик (Webpack, Vite) преобразует имена классов в уникальные хэши. Это даёт изоляцию без соглашений вроде BEM и без накладных расходов CSS-in-JS в runtime.

Где используется

  • React-приложения (Create React App, Vite — поддержка из коробки)
  • Vue (аналог — <style scoped>)
  • Next.js — встроенная поддержка файлов *.module.css
  • Любой проект на Webpack или Vite без желания добавлять CSS-in-JS библиотеку

Как работает CSS Modules

/* Button.module.css */
.button {
  padding: 8px 16px;
  border-radius: 4px;
  font-size: 14px;
}

.button--primary {
  background: #0070f3;
  color: white;
}

.button--secondary {
  background: transparent;
  border: 1px solid #0070f3;
  color: #0070f3;
}
// Button.jsx
import styles from './Button.module.css';

function Button({ children, variant = 'primary', onClick }) {
  return (
    <button
      className={`${styles.button} ${styles[`button--${variant}`]}`}
      onClick={onClick}
    >
      {children}
    </button>
  );
}

После сборки класс .button превращается в нечто вроде .Button_button__xK7mQ — уникальное для этого компонента.

Композиция классов

/* utils.module.css */
.flex-center {
  display: flex;
  align-items: center;
  justify-content: center;
}

/* Card.module.css */
.card {
  composes: flex-center from './utils.module.css';
  border: 1px solid #eee;
  border-radius: 8px;
  padding: 16px;
}

Динамические классы (clsx)

import clsx from 'clsx';
import styles from './Button.module.css';

function Button({ children, disabled, variant }) {
  return (
    <button
      className={clsx(styles.button, {
        [styles['button--primary']]: variant === 'primary',
        [styles['button--disabled']]: disabled,
      })}
      disabled={disabled}
    >
      {children}
    </button>
  );
}

Конфигурация в Vite (по умолчанию)

// vite.config.js — CSS Modules работают без настройки
// любой файл *.module.css обрабатывается как CSS Module
// Опционально настроить генерацию имён:
export default defineConfig({
  css: {
    modules: {
      localsConvention: 'camelCase', // .my-class → styles.myClass
      generateScopedName: '[name]__[local]___[hash:base64:5]',
    },
  },
});

Частые ошибки

  • Импортируют без styles. — пишут className="button" вместо className={styles.button}, и класс остаётся незахэшированным.
  • Используют глобальные стили через CSS Modules — для глобального CSS нужен обычный файл без .module.css, или обёртка :global(.classname).
  • Не используют clsx или classnames — конкатенация строк для условных классов становится неудобной.
  • Конфликт с CSS-in-JS — смешивают CSS Modules и Styled Components в одном компоненте без необходимости.

Связанные темы

Ресурсы