With the introduction of Next.js App Router and React Server Components, the styling landscape has changed dramatically. Styled-components, once a popular choice, is no longer recommended due to server-side rendering limitations. This guide covers the industry-standard approach and alternatives for styling in modern Next.js applications.
Table of Contents
- Why Styled-Components Doesn’t Work
- Recommended Solution: Tailwind CSS
- Alternative Solutions
- Migration Strategy
- Best Practices
- Performance Comparison
Why Styled-Components Doesn’t Work Well with App Router
Styled-components has several issues in the App Router:
// ❌ PROBLEMATIC: Styled-components in Server Components
import styled from "styled-components";
const StyledButton = styled.button`
background: blue;
color: white;
padding: 10px 20px;
`;
// This won't work properly in Server Components
export default function ServerComponent() {
return <StyledButton>Click me</StyledButton>;
}
Issues:
- Server/Client Mismatch: Different styles on server vs client
- Hydration Errors: Style inconsistencies during hydration
- Bundle Size: Increases JavaScript bundle size
- Performance: Slower than CSS alternatives
Recommended Solution: Tailwind CSS
Tailwind CSS is the industry standard for Next.js App Router projects. It provides the best developer experience, performance, and ecosystem support while working seamlessly with both server and client components.
Why Tailwind CSS?
- ✅ Industry Standard: Used by 70%+ of modern Next.js projects
- ✅ Zero Runtime: No JavaScript overhead
- ✅ Server Component Compatible: Works perfectly with RSC
- ✅ Rapid Development: Utility-first approach
- ✅ Excellent Ecosystem: Rich plugin ecosystem
- ✅ Built-in Features: Responsive design, dark mode, animations
- ✅ PurgeCSS: Automatically removes unused styles
- ✅ TypeScript Support: Full type safety with plugins
Basic Implementation
// Button.jsx
export default function Button({ children, variant = "primary", ...props }) {
const baseClasses =
"px-4 py-2 rounded-md font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2";
const variantClasses = {
primary: "bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500",
secondary:
"bg-gray-200 text-gray-900 hover:bg-gray-300 focus:ring-gray-500",
danger: "bg-red-600 text-white hover:bg-red-700 focus:ring-red-500",
};
return (
<button className={`${baseClasses} ${variantClasses[variant]}`} {...props}>
{children}
</button>
);
}
Advanced Patterns
// Card.jsx
export default function Card({ children, className = "", ...props }) {
return (
<div
className={`rounded-lg border border-gray-200 bg-white p-6 shadow-md transition-shadow hover:shadow-lg dark:border-gray-700 dark:bg-gray-800 ${className} `}
{...props}
>
{children}
</div>
);
}
// Usage
<Card className="mx-auto max-w-md">
<h2 className="mb-4 text-xl font-semibold text-gray-900 dark:text-white">
Card Title
</h2>
<p className="text-gray-600 dark:text-gray-300">Card content goes here</p>
</Card>;
Setup in Next.js
// tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}",
"./app/**/*.{js,ts,jsx,tsx,mdx}",
],
darkMode: "class", // or 'media'
theme: {
extend: {
colors: {
primary: {
50: "#eff6ff",
500: "#3b82f6",
600: "#2563eb",
700: "#1d4ed8",
},
},
},
},
plugins: [],
};
Alternative Solutions
While Tailwind CSS is recommended, here are other viable options:
/* Button.module.css */
.button {
background: var(--color-primary);
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.2s;
}
.button:hover {
background: var(--color-primary-dark);
}
.button.secondary {
background: var(--color-secondary);
}
// Button.jsx
import styles from "./Button.module.css";
export default function Button({ children, variant = "primary", ...props }) {
return (
<button
className={`${styles.button} ${variant === "secondary" ? styles.secondary : ""}`}
{...props}
>
{children}
</button>
);
}
Benefits:
- ✅ Server Component Compatible: Works perfectly with RSC
- ✅ Scoped Styles: No style conflicts
- ✅ Type Safety: Full TypeScript support
- ✅ Performance: Zero runtime overhead
- ✅ CSS Features: Full CSS power (custom properties, media queries, etc.)
CSS Modules
CSS Modules provide scoped styling with zero runtime overhead:
// Button.jsx
export default function Button({ children, variant = "primary", ...props }) {
const baseClasses =
"px-4 py-2 rounded-md font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2";
const variantClasses = {
primary: "bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500",
secondary:
"bg-gray-200 text-gray-900 hover:bg-gray-300 focus:ring-gray-500",
danger: "bg-red-600 text-white hover:bg-red-700 focus:ring-red-500",
};
return (
<button className={`${baseClasses} ${variantClasses[variant]}`} {...props}>
{children}
</button>
);
}
Benefits:
- ✅ Utility-First: Rapid development
- ✅ PurgeCSS: Automatically removes unused styles
- ✅ Responsive: Built-in responsive utilities
- ✅ Dark Mode: Easy dark mode implementation
- ✅ Server Compatible: Works with all components
CSS-in-JS with Emotion
For teams that prefer CSS-in-JS, Emotion provides proper SSR support:
// Button.jsx
"use client";
import { css } from "@emotion/react";
const buttonStyles = css`
background: var(--color-primary);
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.2s;
&:hover {
background: var(--color-primary-dark);
}
`;
export default function Button({ children, ...props }) {
return (
<button css={buttonStyles} {...props}>
{children}
</button>
);
}
Setup in Next.js:
// app/layout.jsx
import { CacheProvider } from "@emotion/react";
import createCache from "@emotion/cache";
const cache = createCache({
key: "css",
prepend: true,
});
export default function RootLayout({ children }) {
return (
<html>
<body>
<CacheProvider value={cache}>{children}</CacheProvider>
</body>
</html>
);
}
Global CSS
For simple projects, global CSS with custom properties works well:
/* globals.css */
:root {
--color-primary: #2563eb;
--color-primary-dark: #1d4ed8;
--color-secondary: #6b7280;
--color-secondary-dark: #4b5563;
--color-danger: #dc2626;
--color-danger-dark: #b91c1c;
}
.button {
background: var(--color-primary);
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.2s;
font-weight: 500;
}
.button:hover {
background: var(--color-primary-dark);
}
.button.secondary {
background: var(--color-secondary);
}
.button.secondary:hover {
background: var(--color-secondary-dark);
}
.button.danger {
background: var(--color-danger);
}
.button.danger:hover {
background: var(--color-danger-dark);
}
// Button.jsx
export default function Button({ children, variant = "primary", ...props }) {
const className = `button ${variant !== "primary" ? variant : ""}`;
return (
<button className={className} {...props}>
{children}
</button>
);
}
Best Practices
1. Use Tailwind CSS as Your Primary Solution
// ✅ RECOMMENDED: Tailwind for most components
export default function Card({ children, className = "" }) {
return (
<div
className={`rounded-lg border border-gray-200 bg-white p-6 shadow-md transition-shadow hover:shadow-lg dark:border-gray-700 dark:bg-gray-800 ${className} `}
>
{children}
</div>
);
}
2. Handle Dynamic Styles
// ✅ GOOD: Conditional classes with Tailwind
const Button = ({ variant = "primary", children, ...props }) => {
const baseClasses = "px-4 py-2 rounded-md font-medium transition-colors";
const variantClasses = {
primary: "bg-blue-600 text-white hover:bg-blue-700",
secondary: "bg-gray-200 text-gray-900 hover:bg-gray-300",
danger: "bg-red-600 text-white hover:bg-red-700",
};
return (
<button className={`${baseClasses} ${variantClasses[variant]}`} {...props}>
{children}
</button>
);
};
3. Responsive Design
// ✅ GOOD: Tailwind responsive utilities
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
<div className="rounded-lg bg-white p-4 shadow">Card 1</div>
<div className="rounded-lg bg-white p-4 shadow">Card 2</div>
<div className="rounded-lg bg-white p-4 shadow">Card 3</div>
</div>
4. Dark Mode Support
// ✅ GOOD: Tailwind dark mode classes
<div className="rounded-lg bg-white p-6 text-gray-900 dark:bg-gray-900 dark:text-white">
<h2 className="mb-4 text-xl font-semibold">Title</h2>
<p className="text-gray-600 dark:text-gray-300">Content</p>
</div>
Migration Strategy
From Styled-Components to Tailwind CSS
Step 1: Install Tailwind CSS
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
Step 2: Convert Components
// Before: Styled-components
const StyledButton = styled.button`
background: ${(props) => (props.primary ? "blue" : "gray")};
color: white;
padding: 10px 20px;
border-radius: 4px;
`;
// After: Tailwind CSS
const Button = ({ primary, children, ...props }) => {
const baseClasses = "px-4 py-2 rounded text-white";
const variantClasses = primary
? "bg-blue-600 hover:bg-blue-700"
: "bg-gray-600 hover:bg-gray-700";
return (
<button className={`${baseClasses} ${variantClasses}`} {...props}>
{children}
</button>
);
};
Step 3: Update Imports
// Before
import { StyledButton } from "./StyledButton";
// After
import Button from "./Button";
Performance Comparison
Solution | Bundle Size | Runtime | SSR Support | Overall |
---|---|---|---|---|
Tailwind CSS | 3-10KB | None | ✅ Perfect | ⭐⭐⭐⭐⭐ |
CSS Modules | 0KB | None | ✅ Perfect | ⭐⭐⭐⭐⭐ |
Emotion | 7KB | Medium | ✅ Good | ⭐⭐⭐⭐ |
Styled-components | 15KB | High | ❌ Poor | ⭐⭐ |
Key Metrics
- Bundle Size: CSS/JS code sent to browser
- Runtime: JavaScript processing overhead
- SSR Support: React Server Component compatibility
- Overall: Combined performance and developer experience
Recommendation: Tailwind CSS provides the best balance of performance, developer experience, and ecosystem support.
Conclusion
Use Tailwind CSS as your primary styling solution for Next.js App Router projects.
Why Tailwind CSS?
- Industry Standard: Used by 70%+ of modern Next.js projects
- Perfect SSR Compatibility: Works seamlessly with React Server Components
- Zero Runtime Overhead: No JavaScript performance impact
- Excellent Developer Experience: Rapid development with utility classes
- Rich Ecosystem: Extensive plugin ecosystem and community support
Migration Path
- Start with Tailwind CSS for new projects
- Migrate existing styled-components to Tailwind CSS
- Use CSS Modules only for complex, custom components
- Consider Emotion only if you absolutely need CSS-in-JS
Key Takeaway
The styling landscape has evolved with App Router. Styled-components is no longer the best choice. Tailwind CSS is the industry standard that provides the best performance, developer experience, and ecosystem support while working perfectly with both server and client components.
Your styling approach should enhance, not hinder, the benefits of React Server Components! 🚀