Comparisons

Tailwind vs CSS Modules: AI Rules for Each

Tailwind and CSS Modules are opposite approaches to styling components. Each needs specific AI rules for class naming, component extraction, responsive design, dark mode, and preventing the AI from mixing approaches.

7 min readยทMay 2, 2025

Tailwind utilities in a CSS Modules component, .module.css in a Tailwind project โ€” mixing is the worst option

Utility classes vs scoped selectors, component extraction, responsive, dark mode, and rule templates

Opposite Philosophies for the Same Problem

Tailwind CSS: utility-first classes applied directly in JSX. <div className="flex items-center gap-4 rounded-lg bg-white p-6 shadow-md">. No separate CSS file. No custom class names. The styling is inline with the markup. Tailwind's philosophy: the styling IS the component structure. You read the classes to understand the visual layout. Tailwind generates only the CSS you use (tree-shaking), resulting in small bundles.

CSS Modules: scoped CSS classes in separate .module.css files. import styles from './Card.module.css'; <div className={styles.card}>. The CSS file: .card { display: flex; align-items: center; gap: 1rem; border-radius: 0.5rem; background: white; padding: 1.5rem; box-shadow: 0 4px 6px rgba(0,0,0,0.1); }. CSS Modules' philosophy: separate concerns (markup and styling in different files), meaningful class names (.card not flex items-center), and full CSS power (pseudo-elements, complex selectors, animations).

Without styling rules: AI mixes both approaches in one project (Tailwind classes on some components, CSS Modules on others), generates custom CSS classes when Tailwind utilities exist, writes className={styles.flex} (using CSS Modules to name Tailwind-like utilities), or creates .module.css files in a Tailwind project. The styling approach must be consistent across the project. One rule prevents the mix.

Class Patterns: Utility Strings vs Scoped Names

Tailwind class patterns: className="flex items-center justify-between p-4 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors". Long utility strings in the markup. Conditional classes: className={cn("p-4 rounded-lg", isActive && "bg-blue-500 text-white", !isActive && "bg-gray-100")} using the cn() utility (clsx + tailwind-merge). AI rule: 'Tailwind: use utility classes directly in className. Use cn() for conditional classes. Never create custom CSS classes for styles that Tailwind utilities can express.'

CSS Modules class patterns: className={styles.container} or className={`${styles.card} ${isActive ? styles.active : ''}`}. Short, semantic class names in JSX. The visual details are in the .module.css file. Conditional classes: use template literals or classnames library. AI rule: 'CSS Modules: import styles from .module.css. Use semantic class names (styles.card, styles.header). Conditional: template literals or classnames(). Write CSS in the module file, never inline styles.'

The class rule prevents: AI generating bg-gray-50 rounded-lg in a CSS Modules project (these are Tailwind utilities, not CSS Module class names), creating .module.css files with utility-like classes (.flex, .p-4) instead of semantic classes, or mixing both (className="flex" on one element and className={styles.card} on the next). The class pattern is the most visible expression of the styling choice โ€” one rule makes every component consistent.

  • Tailwind: utility strings in className, cn() for conditionals, no custom CSS files
  • CSS Modules: semantic names from .module.css, template literals for conditionals
  • Tailwind: className='flex p-4 bg-gray-50'. CSS Modules: className={styles.card}
  • cn() = clsx + tailwind-merge: handles conditional Tailwind classes and merges conflicts
  • AI mixing: Tailwind utilities in CSS Module files or Module imports in Tailwind projects
๐Ÿ’ก The Class Pattern Is the Most Visible Rule

Tailwind: className='flex p-4 bg-gray-50'. CSS Modules: className={styles.card}. Every component, every element, every PR review โ€” the class pattern is the most frequently written code in any frontend project. One rule makes every component consistent.

Component Extraction: Reusable Components vs Shared Classes

Tailwind component extraction: when a utility string is repeated, extract a React component (not a CSS class). Repeated: <button className="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600">. Extract: function Button({ children, ...props }) { return <button className={cn("px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600", props.className)} {...props}>{children}</button>; }. The component IS the reuse unit, not a shared CSS class. AI rule: 'Tailwind: extract repeated utility strings into React components, not @apply classes. @apply only in base styles (button resets, typography).'

CSS Modules component extraction: when styles are shared, create shared CSS classes or shared module files. A shared .module.css: .button { padding: 0.5rem 1rem; background: var(--color-primary); color: white; border-radius: 0.5rem; } .button:hover { background: var(--color-primary-hover); }. Import in multiple components. Or: co-locate styles with the component (Button.module.css alongside Button.tsx). AI rule: 'CSS Modules: co-locate styles with components (Button.module.css + Button.tsx). Share common styles via shared module or CSS custom properties.'

The extraction rule prevents: AI using @apply extensively in Tailwind (the Tailwind team discourages this โ€” it defeats the purpose of utility classes), creating shared .module.css files in a Tailwind project (use React components for reuse), or putting all styles in one global CSS file in a CSS Modules project (defeats scoping). The reuse mechanism is approach-determined: Tailwind = React components. CSS Modules = shared module files.

โš ๏ธ Extract Components, Not @apply Classes

Tailwind repeated utility string: extract a React component (Button, Card), not an @apply class. The Tailwind team discourages @apply โ€” it creates CSS classes that defeat utility-first. The React component IS the reuse mechanism. CSS Modules: shared module files are the reuse mechanism.

Responsive Design and Dark Mode

Tailwind responsive: breakpoint prefixes. className="text-sm md:text-base lg:text-lg". Mobile-first: base classes for mobile, md: for tablet, lg: for desktop. Dark mode: dark: variant. className="bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100". The responsive and dark mode patterns are inline with the component โ€” you see all states at once. AI rule: 'Tailwind responsive: mobile-first, md:/lg:/xl: prefixes. Dark mode: dark: variant. All states visible in the className string.'

CSS Modules responsive: @media queries in the .module.css file. .card { font-size: 0.875rem; } @media (min-width: 768px) { .card { font-size: 1rem; } }. Dark mode: @media (prefers-color-scheme: dark) { .card { background: var(--color-bg-dark); } } or [data-theme='dark'] .card { ... }. Responsive and dark mode are in the CSS file, separate from the component JSX. AI rule: 'CSS Modules responsive: @media queries in the .module.css. Dark mode: prefers-color-scheme or data-theme selector. States in CSS, not in JSX.'

The responsive rule prevents: AI writing @media queries in a Tailwind project (use breakpoint prefixes instead), using dark: variant in a CSS Modules project (dark: is Tailwind syntax), or mixing responsive approaches (some components with Tailwind prefixes, others with @media queries). The responsive and dark mode mechanism must be consistent โ€” one rule aligns every responsive and themed component.

  • Tailwind responsive: md:text-lg lg:text-xl โ€” breakpoint prefix in className
  • CSS Modules responsive: @media (min-width: 768px) โ€” in the .module.css file
  • Tailwind dark: dark:bg-gray-900 โ€” variant in className
  • CSS Modules dark: @media (prefers-color-scheme: dark) or [data-theme] selector
  • AI mixing responsive approaches = inconsistent breakpoints across components

When to Choose Each Approach

Choose Tailwind when: your team prefers co-located styling (markup + styles in one file), you want design system consistency (Tailwind config is the single source of design tokens), you value fast prototyping (add classes without creating CSS files), your project uses a component library (shadcn/ui, Radix + Tailwind), or your bundler tree-shakes unused styles (Tailwind only generates used CSS). Most new React and Next.js projects in 2026 use Tailwind.

Choose CSS Modules when: your team prefers separation of concerns (CSS in CSS files, markup in JSX), you need complex CSS features that Tailwind cannot express (pseudo-elements, complex animations, container queries with custom logic), your project has a mature CSS codebase (migrating to Tailwind is costly), or your team has strong CSS expertise and finds utility classes harder to read than semantic class names.

Both are valid choices with different trade-offs. Tailwind: faster to write, larger JSX, design tokens enforced by config. CSS Modules: cleaner JSX, separate CSS files, full CSS power. The choice is team preference and project context โ€” not technical superiority. The critical rule: pick one and use it consistently. Mixing is worse than either approach alone.

โ„น๏ธ Pick One, Use Consistently

Mixing Tailwind and CSS Modules in one project: some components with utility strings, others with .module.css imports. Inconsistent responsive patterns. Inconsistent dark mode. The critical rule is not which to choose โ€” it is to choose one and enforce it everywhere.

Ready-to-Use Rule Templates

Tailwind CLAUDE.md template: '# Styling (Tailwind CSS). Use Tailwind utility classes in className. Conditional classes: cn() utility (clsx + tailwind-merge). Responsive: mobile-first with md:/lg:/xl: prefixes. Dark mode: dark: variant. Component reuse: extract React components, not @apply classes. Design tokens: tailwind.config.ts (colors, spacing, fonts). Never: create .module.css files, write custom CSS for layout/spacing/colors, or use inline style={{}}.'

CSS Modules CLAUDE.md template: '# Styling (CSS Modules). Co-locate styles: Component.module.css alongside Component.tsx. Import: import styles from "./Component.module.css". Semantic class names: .card, .header, .activeState โ€” not utility names. Responsive: @media queries in the module file. Dark mode: prefers-color-scheme or [data-theme] selectors. Shared styles: shared.module.css or CSS custom properties. Never: Tailwind utility classes, cn() utility, or breakpoint prefix syntax (md:, lg:).'

The templates draw a clear styling boundary. Tailwind: everything in className strings, no CSS files. CSS Modules: everything in .module.css files, semantic class names in JSX. The negative rules prevent: Tailwind utilities in CSS Modules projects, .module.css files in Tailwind projects, and the subtle mixing that makes stylesheets inconsistent and unmaintainable.

Comparison Summary

Summary of Tailwind vs CSS Modules AI rules.

  • Classes: Tailwind utilities in className vs CSS Modules semantic names from .module.css
  • Reuse: Tailwind = extract React components. CSS Modules = shared module files
  • Responsive: Tailwind md:/lg: prefixes vs CSS Modules @media queries in CSS files
  • Dark mode: Tailwind dark: variant vs CSS Modules prefers-color-scheme / [data-theme]
  • Conditionals: Tailwind cn() utility vs CSS Modules template literals / classnames()
  • Co-location: Tailwind = styles in JSX. CSS Modules = styles in separate .module.css
  • 2026 default: most new React/Next.js projects use Tailwind. CSS Modules for mature CSS codebases
  • Critical rule: pick one approach and use it consistently โ€” mixing is worse than either alone