Tailwind CSSSeniorSystem design

Как интегрировать Tailwind CSS с CSS Modules или CSS-in-JS решениями?

Tailwind интегрируется с CSS Modules через @apply в .module.css-файлах или совместное использование в компоненте; с CSS-in-JS (Emotion, styled-components) — через twMerge или передачу className через cx(). Обе стратегии требуют осознанного выбора границ между системами.

Интеграция Tailwind CSS с CSS Modules и CSS-in-JS

Tailwind, CSS Modules и CSS-in-JS решают схожие задачи разными методами. Их совместное использование требует чёткого разграничения ответственности, иначе команда получает три конкурирующих стиля в одном проекте.

Tailwind + CSS Modules

Наиболее распространённый сценарий — использование @apply в .module.css для создания именованных классов из utility-токенов:

/* Card.module.css */
.root {
  @apply flex flex-col gap-4 rounded-xl border border-gray-200 bg-white p-6 shadow-sm;
  @apply transition-shadow duration-200 hover:shadow-md;
}

.title {
  @apply text-lg font-semibold text-gray-900;
}

.badge {
  @apply inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium;
}

.badge--success { @apply bg-green-100 text-green-800; }
.badge--error   { @apply bg-red-100   text-red-800;   }
// Card.tsx
import styles from './Card.module.css';

interface CardProps {
  title: string;
  status: 'success' | 'error';
}

export function Card({ title, status }: CardProps) {
  return (
    <article className={styles.root}>
      <h2 className={styles.title}>{title}</h2>
      <span className={`${styles.badge} ${styles[`badge--${status}`]}`}>
        {status}
      </span>
    </article>
  );
}

Этот подход даёт читаемые имена классов в DevTools и инкапсуляцию стилей, сохраняя использование дизайн-токенов Tailwind.

Прямое смешение в одном компоненте

CSS Modules для структурных/сложных стилей, Tailwind — для быстрых утилит:

import styles from './Layout.module.css';

export function Layout({ children }: { children: React.ReactNode }) {
  return (
    <div className={`${styles.container} mt-8 px-4`}>
      {children}
    </div>
  );
}

Tailwind + Emotion (CSS-in-JS)

С Emotion используют cx() для слияния классов или css-тег с Tailwind-переменными:

import { css, cx } from '@emotion/css';

const complexAnimation = css`
  animation: slideIn 300ms cubic-bezier(0.16, 1, 0.3, 1);
  @keyframes slideIn {
    from { transform: translateY(-8px); opacity: 0; }
    to   { transform: translateY(0);    opacity: 1; }
  }
`;

export function Tooltip({ children, label }: TooltipProps) {
  return (
    <div className="relative inline-block">
      {children}
      <div
        className={cx(
          'absolute bottom-full left-1/2 -translate-x-1/2 mb-2',
          'rounded-lg bg-gray-900 px-3 py-1.5 text-sm text-white shadow-lg',
          complexAnimation
        )}
      >
        {label}
      </div>
    </div>
  );
}

Tailwind + styled-components через tailwind-styled-components

import tw from 'tailwind-styled-components';

const StyledButton = tw.button<{ $primary?: boolean }>`
  inline-flex items-center rounded-lg px-4 py-2 font-medium transition-colors
  ${(p) => p.$primary ? 'bg-indigo-600 text-white hover:bg-indigo-700' : 'bg-gray-100 text-gray-900'}
`;

export function Button({ primary, children, ...props }) {
  return <StyledButton $primary={primary} {...props}>{children}</StyledButton>;
}

Разрешение конфликтов с tailwind-merge

При передаче className извне компонента возникают конфликты утилит (p-4 vs p-8). twMerge корректно разрешает их:

import { twMerge } from 'tailwind-merge';

function Card({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
  return (
    <div
      className={twMerge('rounded-xl bg-white p-6 shadow-sm', className)}
      {...props}
    />
  );
}

// Использование — p-6 будет заменён на p-10
<Card className="p-10 shadow-lg" />

Подводные камни

  • @apply в .module.css работает только если PostCSS обрабатывает файл с Tailwind-плагином — не все конфигурации Webpack это делают.
  • Emotion SSR + Tailwind: без ServerStyleSheet или CacheProvider возможен FOUC.
  • Specificity войны: CSS Modules генерируют [hash]-классы со specificity 0-1-0, Tailwind — тоже 0-1-0. Порядок в <head> решает всё.
  • tailwind-styled-components не поддерживает Tailwind v4 — нужна проверка совместимости.
  • Два источника токенов (CSS-переменные Tailwind и тема Emotion ThemeProvider) расходятся без синхронизации.
  • Tree-shaking CSS-in-JS-стилей при SSG работает иначе, чем Tailwind PurgeCSS — лишние стили могут попасть в bundle.
  • DevTools показывают нечитаемые Emotion-хэши рядом с читаемыми Tailwind-классами — дебаг усложняется.

Common mistakes

  • Смешивать «Tailwind с CSS Modules или CSS-in-JS» с похожим механизмом без критерия выбора.
  • Игнорировать риск: неверно оценить границы применения темы «Tailwind с CSS Modules или CSS-in-JS» и получить хрупкое решение.
  • Показывать только синтаксис и не объяснять поведение в runtime или сборке.

What the interviewer is testing

  • Объясняет границы utility-first подхода и интеграция с локальными стилями.
  • Показывает на примере, как работает: Tailwind обычно живет в className, а CSS Modules или CSS-in-JS оставляют для сложных scoped-правил, third-party overrides или runtime-динамики, которую нельзя выразить статическими utilities.
  • Называет production-нюанс и граничный случай для темы «Tailwind с CSS Modules или CSS-in-JS».

Sources

Related topics