CSS Modules solve one of CSS's biggest problems: naming conflicts. When building large applications, it's hard to ensure class names don't clash. CSS Modules provide automatic scoping—each component's styles are isolated, class names are unique, and you can use simple names like .button without worrying about conflicts. Next.js has built-in CSS Modules support with zero configuration. Let's master component-scoped styling!
What Are CSS Modules?
CSS Modules are CSS files where class names are scoped locally by default:
❌ Regular CSS (Global)
/* All .button classes are global */
.button {
padding: 10px 20px;
background: blue;
}
/* This affects ALL buttons in your app! */// Regular CSS
import './styles.css';
export function Button() {
return <button className="button">Click</button>;
}
// ❌ Problem: .button is global
// ❌ Conflicts with other .button classes
// ❌ Hard to maintain in large apps✅ CSS Modules (Scoped)
/* Scoped to this component only */
.button {
padding: 10px 20px;
background: blue;
}
/* Only affects buttons in this component */// CSS Module
import styles from './Button.module.css';
export function Button() {
return <button className={styles.button}>Click</button>;
}
// ✅ Scoped to this component
// ✅ No naming conflicts
// ✅ Easy to maintainHow CSS Modules Work
CSS Modules generate unique class names automatically. Your .button becomes something like .Button_button__2x3y4. You write simple names, Next.js handles uniqueness!
Basic CSS Module Usage
Step 1: Create a CSS Module
Create a CSS file with the .module.css extension:
.button {
padding: 12px 24px;
border-radius: 8px;
font-weight: 600;
border: none;
cursor: pointer;
transition: all 0.2s;
}
.button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.primary {
background: #3b82f6;
color: white;
}
.secondary {
background: #6b7280;
color: white;
}
.outline {
background: transparent;
border: 2px solid #3b82f6;
color: #3b82f6;
}
.outline:hover {
background: #3b82f6;
color: white;
}
// ✅ Simple, readable class names
// ✅ No need for BEM or complex naming
// ✅ Automatically scopedStep 2: Import and Use
import styles from './Button.module.css';
interface ButtonProps {
children: React.ReactNode;
variant?: 'primary' | 'secondary' | 'outline';
onClick?: () => void;
}
export function Button({
children,
variant = 'primary',
onClick
}: ButtonProps) {
return (
<button
className={`${styles.button} ${styles[variant]}`}
onClick={onClick}
>
{children}
</button>
);
}
// Usage:
// <Button variant="primary">Click Me</Button>
// <Button variant="outline">Cancel</Button>
// ✅ Import styles as an object
// ✅ Access classes as properties: styles.button
// ✅ Combine multiple classes with template literalsAdvanced CSS Module Patterns
Pattern 1: Conditional Classes
import styles from './Alert.module.css';
interface AlertProps {
type: 'success' | 'error' | 'warning' | 'info';
children: React.ReactNode;
dismissible?: boolean;
}
export function Alert({ type, children, dismissible }: AlertProps) {
return (
<div className={`${styles.alert} ${styles[type]}`}>
<div className={styles.content}>
{children}
</div>
{dismissible && (
<button className={styles.closeButton}>×</button>
)}
</div>
);
}
// ✅ Dynamic class based on type
// ✅ Conditional rendering with styles
// ✅ Clean component code.alert {
padding: 16px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
.success {
background: #d1fae5;
border: 1px solid #10b981;
color: #065f46;
}
.error {
background: #fee2e2;
border: 1px solid #ef4444;
color: #991b1b;
}
.warning {
background: #fef3c7;
border: 1px solid #f59e0b;
color: #92400e;
}
.info {
background: #dbeafe;
border: 1px solid #3b82f6;
color: #1e40af;
}
.content {
flex: 1;
}
.closeButton {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
padding: 0 8px;
color: inherit;
opacity: 0.6;
}
.closeButton:hover {
opacity: 1;
}Pattern 2: Composition (Sharing Styles)
/* Base card styles */
.card {
background: white;
border-radius: 12px;
padding: 24px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
transition: box-shadow 0.2s;
}
/* Inherit from card and add hover effect */
.cardHoverable {
composes: card;
cursor: pointer;
}
.cardHoverable:hover {
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);
}
/* Inherit from card and add border */
.cardBordered {
composes: card;
border: 2px solid #e5e7eb;
}
/* Inherit and customize */
.cardPrimary {
composes: card;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
// ✅ composes: shares styles between classes
// ✅ DRY principle - no duplication
// ✅ Flexible variationsimport styles from './Card.module.css';
interface CardProps {
children: React.ReactNode;
variant?: 'default' | 'hoverable' | 'bordered' | 'primary';
}
export function Card({ children, variant = 'default' }: CardProps) {
const cardStyles = {
default: styles.card,
hoverable: styles.cardHoverable,
bordered: styles.cardBordered,
primary: styles.cardPrimary,
};
return (
<div className={cardStyles[variant]}>
{children}
</div>
);
}
// ✅ Cleaner than string concatenation
// ✅ Type-safe variants
// ✅ Easy to maintainPattern 3: Global Selectors (When Needed)
.richText {
/* Container styles */
max-width: 65ch;
line-height: 1.7;
}
/* Target global HTML elements inside */
.richText :global(h1) {
font-size: 2.5rem;
margin-bottom: 1rem;
}
.richText :global(h2) {
font-size: 2rem;
margin-bottom: 0.75rem;
}
.richText :global(p) {
margin-bottom: 1rem;
}
.richText :global(a) {
color: #3b82f6;
text-decoration: underline;
}
.richText :global(code) {
background: #f3f4f6;
padding: 2px 6px;
border-radius: 4px;
font-family: monospace;
}
// ✅ :global() for targeting HTML elements
// ✅ Useful for markdown/HTML content
// ✅ Still scoped to .richText containerimport styles from './RichText.module.css';
interface RichTextProps {
html: string;
}
export function RichText({ html }: RichTextProps) {
return (
<div
className={styles.richText}
dangerouslySetInnerHTML={{ __html: html }}
/>
);
}
// ✅ Styles all HTML elements inside
// ✅ Perfect for CMS content or markdown
// ✅ Contained within componentComplete Component Examples
Example 1: Button Component with Variants
import styles from './Button.module.css';
interface ButtonProps {
children: React.ReactNode;
variant?: 'primary' | 'secondary' | 'danger' | 'ghost';
size?: 'small' | 'medium' | 'large';
fullWidth?: boolean;
disabled?: boolean;
onClick?: () => void;
}
export function Button({
children,
variant = 'primary',
size = 'medium',
fullWidth = false,
disabled = false,
onClick,
}: ButtonProps) {
const buttonClasses = [
styles.button,
styles[variant],
styles[size],
fullWidth && styles.fullWidth,
disabled && styles.disabled,
]
.filter(Boolean)
.join(' ');
return (
<button
className={buttonClasses}
disabled={disabled}
onClick={onClick}
>
{children}
</button>
);
}.button {
border-radius: 8px;
font-weight: 600;
border: none;
cursor: pointer;
transition: all 0.2s;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
}
/* Variants */
.primary {
background: #3b82f6;
color: white;
}
.primary:hover {
background: #2563eb;
}
.secondary {
background: #6b7280;
color: white;
}
.secondary:hover {
background: #4b5563;
}
.danger {
background: #ef4444;
color: white;
}
.danger:hover {
background: #dc2626;
}
.ghost {
background: transparent;
color: #374151;
border: 1px solid #d1d5db;
}
.ghost:hover {
background: #f3f4f6;
}
/* Sizes */
.small {
padding: 6px 12px;
font-size: 14px;
}
.medium {
padding: 10px 20px;
font-size: 16px;
}
.large {
padding: 14px 28px;
font-size: 18px;
}
/* Modifiers */
.fullWidth {
width: 100%;
}
.disabled {
opacity: 0.5;
cursor: not-allowed;
pointer-events: none;
}
// ✅ Complete button system
// ✅ Multiple variants and sizes
// ✅ Modifier classes
// ✅ Accessible (disabled state)Example 2: Card Component with Nested Elements
import styles from './Card.module.css';
interface CardProps {
title?: string;
description?: string;
children?: React.ReactNode;
image?: string;
footer?: React.ReactNode;
}
export function Card({
title,
description,
children,
image,
footer
}: CardProps) {
return (
<div className={styles.card}>
{image && (
<div className={styles.imageContainer}>
<img src={image} alt={title} className={styles.image} />
</div>
)}
<div className={styles.content}>
{title && (
<h3 className={styles.title}>{title}</h3>
)}
{description && (
<p className={styles.description}>{description}</p>
)}
{children && (
<div className={styles.body}>{children}</div>
)}
</div>
{footer && (
<div className={styles.footer}>{footer}</div>
)}
</div>
);
}
// Usage:
// <Card
// title="Product Name"
// description="Product description"
// image="/product.jpg"
// footer={<Button>Add to Cart</Button>}
// />.card {
background: white;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
transition: box-shadow 0.2s;
}
.card:hover {
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);
}
.imageContainer {
width: 100%;
height: 200px;
overflow: hidden;
background: #f3f4f6;
}
.image {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s;
}
.card:hover .image {
transform: scale(1.05);
}
.content {
padding: 20px;
}
.title {
font-size: 20px;
font-weight: 700;
margin-bottom: 8px;
color: #111827;
}
.description {
font-size: 14px;
color: #6b7280;
line-height: 1.6;
margin-bottom: 12px;
}
.body {
margin-top: 16px;
}
.footer {
padding: 16px 20px;
border-top: 1px solid #e5e7eb;
background: #f9fafb;
}
// ✅ Organized nested elements
// ✅ Hover effects
// ✅ Responsive image
// ✅ Clean structureExample 3: Form Input Component
import styles from './Input.module.css';
interface InputProps {
label?: string;
error?: string;
helperText?: string;
fullWidth?: boolean;
required?: boolean;
type?: 'text' | 'email' | 'password' | 'number';
placeholder?: string;
value?: string;
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
}
export function Input({
label,
error,
helperText,
fullWidth = false,
required = false,
type = 'text',
placeholder,
value,
onChange,
}: InputProps) {
return (
<div className={`${styles.container} ${fullWidth ? styles.fullWidth : ''}`}>
{label && (
<label className={styles.label}>
{label}
{required && <span className={styles.required}>*</span>}
</label>
)}
<input
type={type}
className={`${styles.input} ${error ? styles.inputError : ''}`}
placeholder={placeholder}
value={value}
onChange={onChange}
required={required}
/>
{error && (
<span className={styles.error}>{error}</span>
)}
{helperText && !error && (
<span className={styles.helperText}>{helperText}</span>
)}
</div>
);
}.container {
display: inline-block;
min-width: 200px;
}
.fullWidth {
width: 100%;
}
.label {
display: block;
font-size: 14px;
font-weight: 600;
color: #374151;
margin-bottom: 6px;
}
.required {
color: #ef4444;
margin-left: 4px;
}
.input {
width: 100%;
padding: 10px 14px;
border: 2px solid #d1d5db;
border-radius: 8px;
font-size: 16px;
transition: all 0.2s;
background: white;
}
.input:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.inputError {
border-color: #ef4444;
}
.inputError:focus {
border-color: #ef4444;
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1);
}
.error {
display: block;
margin-top: 6px;
font-size: 14px;
color: #ef4444;
}
.helperText {
display: block;
margin-top: 6px;
font-size: 14px;
color: #6b7280;
}
// ✅ Complete input component
// ✅ Error states
// ✅ Focus styles
// ✅ Accessible (label, required)CSS Modules Project Structure
Organizing components with CSS Modules
Select a file or folder to see details
CSS Modules Best Practices
1. Co-locate Styles with Components
// ✅ GOOD: Component and styles together
components/
Button/
Button.tsx
Button.module.css
Card/
Card.tsx
Card.module.css
// ❌ BAD: Styles separated
components/
Button.tsx
Card.tsx
styles/
Button.module.css
Card.module.css
// Co-location makes components self-contained and portable2. Use Simple, Descriptive Names
/* ✅ GOOD: Simple names (scoping handles uniqueness) */
.button { }
.title { }
.content { }
/* ❌ BAD: Overly specific names (unnecessary with scoping) */
.my-app-button-component-primary { }
.page-header-title-text-large { }
/* CSS Modules handle uniqueness - keep names simple! */3. Use camelCase for Multi-Word Classes
/* ✅ GOOD: camelCase */
.primaryButton { }
.errorMessage { }
.navbarLink { }
/* ❌ BAD: kebab-case (requires bracket notation) */
.primary-button { } /* styles['primary-button'] */
.error-message { } /* styles['error-message'] */// camelCase: Clean dot notation
<button className={styles.primaryButton}>
// kebab-case: Bracket notation required
<button className={styles['primary-button']}>4. Avoid Over-Nesting
/* ✅ GOOD: Flat structure */
.card { }
.cardHeader { }
.cardTitle { }
.cardBody { }
.cardFooter { }
/* ❌ BAD: Deep nesting */
.card { }
.card .header { }
.card .header .title { }
.card .body { }
.card .footer { }
/* Flat is easier to maintain and override */5. Combine with Tailwind for Complex Layouts
// ✅ GOOD: CSS Modules for component styles, Tailwind for layout
import styles from './Card.module.css';
export function Card() {
return (
<div className={`${styles.card} container mx-auto px-4`}>
<div className="grid grid-cols-3 gap-6">
<div className={styles.content}>...</div>
</div>
</div>
);
}
// CSS Module: Component-specific styles (button, card, etc.)
// Tailwind: Layout, spacing, responsive designKey Takeaways
- .module.css extension - tells Next.js to scope styles
- Automatic scoping - unique class names generated
- Import as object - styles.className for access
- No naming conflicts - same names in different components OK
- composes: - share styles between classes
- :global() - target global elements when needed
- Co-location - keep styles with components
- camelCase names - cleaner dot notation access
What's Next?
You've mastered CSS Modules! Next, we'll explore Tailwind CSS Setup and Configuration—the popular utility-first CSS framework. You'll learn how to set up Tailwind in Next.js, configure it for your project, customize the theme, and combine it with CSS Modules for the best of both worlds.
Tailwind offers rapid development with utility classes, while CSS Modules provide component-scoped custom styles. Together, they're incredibly powerful!
🎯 When to Use CSS Modules
Use CSS Modules for custom component styles that need detailed control. Use Tailwind for rapid prototyping and layout. Many projects use both—CSS Modules for complex components, Tailwind for everything else!