Skip to main content
{/}codefunc
GuidesJuly 7, 2026 · 7 min read

CSS Architecture: Tailwind vs CSS Modules vs CSS-in-JS

A practical guide to CSS architecture patterns — when to use Tailwind for rapid development, CSS Modules for component scoping, or CSS-in-JS for dynamic styling.

By codefunc

csstailwindcss-modulesstyled-componentsfrontendstyling

Every frontend project needs a CSS strategy. The wrong choice leads to specificity wars, bloated bundles, or unmaintainable stylesheets. In 2026, three approaches dominate: Tailwind for utility-first rapid development, CSS Modules for scoped component styles, and CSS-in-JS for dynamic runtime styling.

Bottom line: Use Tailwind for most projects — it ships less CSS, has excellent DX, and scales well. Use CSS Modules when you want traditional CSS with guaranteed scoping. Use CSS-in-JS (Styled Components, Emotion) when you need heavy runtime theming or are in a styled-components codebase. Avoid mixing multiple approaches in one project.

Quick comparison

Feature Tailwind CSS Modules CSS-in-JS
Learning curve Medium Low Medium
Bundle size Small (purged) Medium Large
Runtime cost None None Yes
Dynamic styles Limited No Yes
Type safety With plugins No Yes
Server components Yes Yes Partial
Tooling required PostCSS CSS loader Runtime library

Tailwind CSS

Tailwind provides utility classes that you compose directly in markup.

How Tailwind works

// Instead of writing CSS...
// .card { padding: 1rem; background: white; border-radius: 0.5rem; }

// You write utilities in markup
<div className="p-4 bg-white rounded-lg shadow-md">
  <h2 className="text-xl font-bold text-gray-900">Card Title</h2>
  <p className="mt-2 text-gray-600">Card content goes here.</p>
</div>

Tailwind setup

npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
// tailwind.config.js
module.exports = {
  content: ['./src/**/*.{js,ts,jsx,tsx}'],
  theme: {
    extend: {
      colors: {
        brand: '#10b981',
      },
    },
  },
  plugins: [],
};
/* src/globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

Tailwind patterns

Extracting components:

// Don't repeat long class strings
// Extract to components instead

function Button({ children, variant = 'primary' }) {
  const base = 'px-4 py-2 rounded-lg font-medium transition-colors';
  const variants = {
    primary: 'bg-blue-600 text-white hover:bg-blue-700',
    secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300',
  };
  
  return (
    <button className={`${base} ${variants[variant]}`}>
      {children}
    </button>
  );
}

Using @apply (sparingly):

/* Only for truly repeated patterns */
@layer components {
  .btn-primary {
    @apply px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700;
  }
}

Responsive design:

<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
  {/* Mobile: 1 column, Tablet: 2 columns, Desktop: 3 columns */}
</div>

Dark mode:

<div className="bg-white dark:bg-gray-900 text-gray-900 dark:text-white">
  Adapts to color scheme
</div>

Tailwind strengths

  • Small production CSS: Only ships used utilities
  • Consistent design: Constrained to design tokens
  • Fast development: No context switching to CSS files
  • Responsive built-in: sm:, md:, lg: prefixes
  • Dark mode built-in: dark: prefix

Tailwind limitations

  • Long class strings in markup
  • Learning utility names takes time
  • Custom designs need configuration
  • Some find it "ugly" in JSX

When to use Tailwind

  • New projects with design system
  • Rapid prototyping
  • Teams comfortable with utility-first
  • Projects prioritizing bundle size

CSS Modules

CSS Modules scope class names to components automatically.

How CSS Modules work

/* Button.module.css */
.button {
  padding: 0.5rem 1rem;
  border-radius: 0.5rem;
  font-weight: 500;
}

.primary {
  background-color: #2563eb;
  color: white;
}

.secondary {
  background-color: #e5e7eb;
  color: #111827;
}
// Button.tsx
import styles from './Button.module.css';

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

Generated HTML:

<button class="Button_button__x7f3k Button_primary__a2b4c">
  Click me
</button>

CSS Modules patterns

Composing styles:

/* base.module.css */
.text {
  font-family: system-ui;
  line-height: 1.5;
}

/* Button.module.css */
.button {
  composes: text from './base.module.css';
  padding: 0.5rem 1rem;
}

Global styles:

/* When you need to target global classes */
.wrapper :global(.third-party-class) {
  color: red;
}

With CSS variables:

.button {
  background-color: var(--color-primary);
  color: var(--color-primary-foreground);
}

CSS Modules strengths

  • Guaranteed scoping: No class name conflicts
  • Standard CSS: No new syntax to learn
  • No runtime: Compiled at build time
  • Works everywhere: Supported by all bundlers
  • Server components: Full compatibility

CSS Modules limitations

  • One file per component
  • No dynamic styles at runtime
  • Verbose for complex conditionals
  • No type checking for class names

When to use CSS Modules

  • Teams comfortable with traditional CSS
  • Projects migrating from BEM or similar
  • When runtime performance is critical
  • React Server Components projects

CSS-in-JS

CSS-in-JS writes styles in JavaScript, enabling dynamic styling.

Styled Components

import styled from 'styled-components';

const Button = styled.button<{ $primary?: boolean }>`
  padding: 0.5rem 1rem;
  border-radius: 0.5rem;
  font-weight: 500;
  background-color: ${props => props.$primary ? '#2563eb' : '#e5e7eb'};
  color: ${props => props.$primary ? 'white' : '#111827'};
  
  &:hover {
    opacity: 0.9;
  }
`;

function App() {
  return (
    <>
      <Button $primary>Primary</Button>
      <Button>Secondary</Button>
    </>
  );
}

Emotion

/** @jsxImportSource @emotion/react */
import { css } from '@emotion/react';

const buttonStyle = (primary: boolean) => css`
  padding: 0.5rem 1rem;
  border-radius: 0.5rem;
  background-color: ${primary ? '#2563eb' : '#e5e7eb'};
  color: ${primary ? 'white' : '#111827'};
`;

function Button({ primary, children }) {
  return <button css={buttonStyle(primary)}>{children}</button>;
}

CSS-in-JS patterns

Theming:

import { ThemeProvider } from 'styled-components';

const theme = {
  colors: {
    primary: '#2563eb',
    secondary: '#e5e7eb',
  },
  spacing: {
    sm: '0.5rem',
    md: '1rem',
  },
};

const Button = styled.button`
  background-color: ${props => props.theme.colors.primary};
  padding: ${props => props.theme.spacing.md};
`;

function App() {
  return (
    <ThemeProvider theme={theme}>
      <Button>Themed Button</Button>
    </ThemeProvider>
  );
}

Extending styles:

const Button = styled.button`
  padding: 0.5rem 1rem;
  border-radius: 0.5rem;
`;

const PrimaryButton = styled(Button)`
  background-color: #2563eb;
  color: white;
`;

CSS-in-JS strengths

  • Dynamic styles: Based on props and state
  • Co-location: Styles with components
  • Type safety: TypeScript support
  • Theming: Built-in theme systems
  • No class name conflicts: Automatic scoping

CSS-in-JS limitations

  • Runtime cost: Styles computed at runtime
  • Bundle size: Library adds ~10-15KB
  • Server components: Limited support
  • Learning curve: New mental model

When to use CSS-in-JS

  • Heavy theming requirements (white-label apps)
  • Existing styled-components codebase
  • Complex dynamic styling needs
  • When type safety for styles is important

Comparing approaches

Same component, three ways

Tailwind:

function Card({ title, highlighted }) {
  return (
    <div className={`p-4 rounded-lg shadow-md ${
      highlighted ? 'bg-yellow-50 border-yellow-200' : 'bg-white'
    }`}>
      <h2 className="text-xl font-bold">{title}</h2>
    </div>
  );
}

CSS Modules:

/* Card.module.css */
.card {
  padding: 1rem;
  border-radius: 0.5rem;
  box-shadow: 0 1px 3px rgba(0,0,0,0.1);
  background-color: white;
}

.highlighted {
  background-color: #fefce8;
  border-color: #fef08a;
}

.title {
  font-size: 1.25rem;
  font-weight: 700;
}
import styles from './Card.module.css';

function Card({ title, highlighted }) {
  return (
    <div className={`${styles.card} ${highlighted ? styles.highlighted : ''}`}>
      <h2 className={styles.title}>{title}</h2>
    </div>
  );
}

Styled Components:

const CardWrapper = styled.div<{ $highlighted?: boolean }>`
  padding: 1rem;
  border-radius: 0.5rem;
  box-shadow: 0 1px 3px rgba(0,0,0,0.1);
  background-color: ${p => p.$highlighted ? '#fefce8' : 'white'};
  border-color: ${p => p.$highlighted ? '#fef08a' : 'transparent'};
`;

const Title = styled.h2`
  font-size: 1.25rem;
  font-weight: 700;
`;

function Card({ title, highlighted }) {
  return (
    <CardWrapper $highlighted={highlighted}>
      <Title>{title}</Title>
    </CardWrapper>
  );
}

Bundle size impact

For a medium app (~50 components):

Approach CSS size JS overhead
Tailwind ~10KB 0
CSS Modules ~30KB 0
Styled Components ~20KB ~15KB runtime

Hybrid approaches

Tailwind + CSS Modules

Use Tailwind for utilities, CSS Modules for complex components:

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

function Component() {
  return (
    <div className={`p-4 ${styles.animatedGradient}`}>
      Content with both Tailwind spacing and custom animation
    </div>
  );
}

Design tokens everywhere

Define tokens once, use in any approach:

:root {
  --color-primary: #2563eb;
  --spacing-md: 1rem;
  --radius-lg: 0.5rem;
}

Works with Tailwind (theme.extend), CSS Modules (var(--color-primary)), and CSS-in-JS.

Tools

For optimizing your CSS output, use the CSS Minifier. Calculate fluid typography with CSS Clamp Calculator. Convert colors between formats with Color Converter. Generate border radius values with Border Radius Generator.

Decision guide

Need heavy runtime theming?
├── Yes → CSS-in-JS (Styled Components)
└── No → Want utility-first?
    ├── Yes → Tailwind
    └── No → CSS Modules

Practical takeaway

Tailwind is the default choice for new projects — it ships small bundles, enforces consistency, and scales well. CSS Modules work best when your team prefers traditional CSS or needs full Server Component support. CSS-in-JS suits apps with complex theming or existing styled-components codebases. Pick one approach and stick with it. Mixing approaches creates maintenance burden.

Sources

Try it on codefunc

Related articles

3

Search tools

Find a developer tool