Как интегрировать 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».