Introduction đ
Next.js is unopinionated about styling â it supports global CSS, CSS Modules, Sass, Tailwind CSS, and CSS-in-JS libraries, all with built-in tooling and zero extra configuration for most cases. This tutorial walks through every major styling approach, along with patterns for theming, responsiveness, and animation.
Information
Styling in Next.js đ§
Because the App Router renders both Server and Client Components, your styling choice matters: some approaches (like Tailwind and CSS Modules) work everywhere, while others (like many CSS-in-JS libraries) require a "use client" boundary.
- Zero-runtime options â Global CSS, CSS Modules, Sass, Tailwind.
- Runtime options â Styled Components, Emotion (require client-side JS).
Global CSS đ
Global CSS is imported once, typically in the root layout, and applies to the entire application. It's best suited for resets, base typography, and truly app-wide styles.
app/globals.css
body {
margin: 0;
font-family: sans-serif;
}app/layout.tsx
import "./globals.css";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return <html lang="en"><body>{children}</body></html>;
}Important
CSS Modules đĻ
CSS Modules scope class names to the individual component file automatically, preventing style collisions across the app. Any file ending in .module.css is treated as a module.
components/Card.module.css
.card {
padding: 16px;
border-radius: 8px;
}components/Card.tsx
import styles from "./Card.module.css";
export default function Card() {
return <div className={styles.card}>Content</div>;
}Sass (SCSS) đ
Next.js has built-in support for Sass â just install the sass package and use .scss or .module.scss files exactly like their CSS counterparts.
components/Card.module.scss
.card {
padding: 16px;
&:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
}Note
Tailwind CSS đ
Tailwind CSS is a utility-first framework that lets you style elements directly with className utilities, and is officially supported out of the box when scaffolding a new Next.js project.
components/Button.tsx
export default function Button() {
return (
<button className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">
Click Me
</button>
);
}Tip
CSS-in-JS đģ
CSS-in-JS libraries write styles directly in JavaScript/TypeScript files. Most require a "use client" directive in the App Router, since they rely on runtime style injection.
Caution
Styled JSX đ¯
Styled JSX is built into Next.js and lets you write scoped CSS directly inside a component using a <style jsx> tag â no extra installation required.
components/Alert.tsx
"use client";
export default function Alert() {
return (
<div className="alert">
This is an alert
<style jsx>{`
.alert {
padding: 12px;
background: #fef3c7;
border-radius: 6px;
}
`}</style>
</div>
);
}By default, Styled JSX scopes the CSS to the component where it is defined. This means styles such as .alert will not accidentally affect elements with the same class name in other components.
If you want a style to apply globally, use <style jsx global> instead. CSS inside this block is not scoped to the current component and can affect matching elements throughout the application.
components/GlobalStyles.tsx
"use client";
export default function GlobalStyles() {
return (
<style jsx global>{`
body {
margin: 0;
font-family: sans-serif;
}
a {
color: inherit;
text-decoration: none;
}
`}</style>
);
}In practice, use <style jsx> for component-specific styles and <style jsx global> when you specifically need global CSS from a component. For application-wide styles such as resets, typography, and CSS variables, a global.css file is often a better choice.
Styled Components đ
Styled Components lets you define components with attached styles using tagged template literals. It requires a "use client" directive and a registry setup for Server Component compatibility.
components/Button.tsx
"use client";
import styled from "styled-components";
const StyledButton = styled.button`
padding: 8px 16px;
background: #2563eb;
color: white;
border-radius: 8px;
`;
export default function Button() {
return <StyledButton>Click Me</StyledButton>;
}Emotion đ
Emotion offers a similar API to Styled Components, with support for both the styled API and the css prop, and also requires a "use client" boundary in the App Router.
components/Badge.tsx
"use client";
import { css } from "@emotion/react";
const badgeStyle = css`
padding: 4px 8px;
background: #dcfce7;
border-radius: 999px;
`;
export default function Badge() {
return <span css={badgeStyle}>New</span>;
}CSS Variables đ§
CSS custom properties (variables) are a lightweight way to share values like colors and spacing across your stylesheets, and integrate naturally with dynamic theming.
app/globals.css
:root {
--color-primary: #2563eb;
--spacing-md: 16px;
}
.button {
background: var(--color-primary);
padding: var(--spacing-md);
}Dynamic Styling âĄ
For styles that depend on runtime state â like a progress bar's width â inline style props or CSS variables set via JavaScript are the most straightforward approach.
components/ProgressBar.tsx
"use client";
export default function ProgressBar({ percent }: { percent: number }) {
return <div style={{ width: `${percent}%`, height: 8, background: "#2563eb" }} />;
}Conditional Styling đ
Conditionally applying class names based on props or state is a common pattern, often assisted by small utility libraries like clsx for readability.
components/Tag.tsx
import clsx from "clsx";
export default function Tag({ active }: { active: boolean }) {
return <span className={clsx("tag", active && "tag-active")}>Tag</span>;
}Theme Management đ¨
Theme management typically centers on a set of CSS variables (or a Tailwind config) that define colors, spacing, and typography, which can then be swapped based on user preference.
app/globals.css
:root {
--bg: white;
--text: black;
}
[data-theme="dark"] {
--bg: #111827;
--text: white;
}Dark Mode đ
Dark mode is commonly implemented by toggling a data-theme attribute on the <html> element, driven by a Client Component that reads and persists the user's preference.
components/ThemeToggle.tsx
"use client";
import { useState, useEffect } from "react";
export default function ThemeToggle() {
const [theme, setTheme] = useState("light");
useEffect(() => {
document.documentElement.setAttribute("data-theme", theme);
}, [theme]);
return (
<button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
Toggle theme
</button>
);
}Light Mode âī¸
Light mode is usually the default theme, defined directly on :root without needing an attribute selector, with dark mode layered on top as an override.
Tip
Custom Themes â¨
Beyond simple light/dark, custom themes (like a "high contrast" or branded theme) can be built the same way â as additional attribute values mapping to their own set of CSS variables.
app/globals.css
[data-theme="high-contrast"] {
--bg: black;
--text: yellow;
}Responsive Design đą
Responsive design ensures your UI adapts to different screen sizes, using media queries, responsive utility classes (in Tailwind), or the Image component's sizes prop for responsive assets.
components/Grid.tsx
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{/* items */}
</div>Media Queries đĨī¸
Media queries apply styles conditionally based on characteristics like viewport width, letting you build layouts that adapt between mobile, tablet, and desktop.
app/globals.css
.sidebar {
display: none;
}
@media (min-width: 1024px) {
.sidebar {
display: block;
}
}Container Queries đĻ
Unlike media queries, container queries respond to the size of a parent container rather than the viewport, making components that adapt correctly regardless of where they're placed.
components/Card.module.css
.wrapper {
container-type: inline-size;
}
@container (min-width: 400px) {
.card {
flex-direction: row;
}
}Custom Fonts đ¤
Custom fonts loaded via next/font expose a className or CSS variable that can be applied globally or scoped to specific components, integrating cleanly with any styling approach.
app/layout.tsx
import { Inter } from "next/font/google";
const inter = Inter({ subsets: ["latin"], variable: "--font-inter" });CSS Architecture đī¸
As a project grows, organizing styles by convention â one CSS Module per component, a shared globals.css for resets, and a theme.css for variables â keeps the codebase maintainable.
Component-Based Styling đ§Š
Component-based styling â where each component owns its own styles via a CSS Module or Styled Component â keeps concerns isolated and avoids one component's styles accidentally leaking into another.
Global Styles vs Local Styles âī¸
| Approach | Scope | Best For |
|---|---|---|
| Global CSS | entire app | resets, typography, variables |
| CSS Modules | single component | component-specific styles |
| Tailwind utilities | per-element | rapid, consistent styling |
Styling Server Components đĨī¸
Server Components can use Global CSS, CSS Modules, Sass, and Tailwind freely, since none of these require client-side JavaScript to function.
app/page.tsx
import styles from "./page.module.css";
export default function Page() {
return <h1 className={styles.title}>Welcome</h1>;
}Note
Styling Client Components đąī¸
Client Components can use every styling approach, including CSS-in-JS libraries like Styled Components and Emotion, since they have access to the full React client runtime.
components/InteractiveCard.tsx
"use client";
import styled from "styled-components";
const Card = styled.div`
padding: 16px;
cursor: pointer;
`;
export default function InteractiveCard() {
return <Card onClick={() => alert("clicked")}>Click me</Card>;
}Animations đŦ
Simple animations can be built with plain CSS @keyframes, while more complex, interactive animations often reach for a library like Framer Motion inside a Client Component.
app/globals.css
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.fade-in {
animation: fadeIn 0.3s ease-in;
}Transitions đ
CSS transition properties smoothly animate changes between states â like hover effects or theme switches â without needing JavaScript at all.
components/Button.module.css
.button {
background: #2563eb;
transition: background 0.2s ease;
}
.button:hover {
background: #1d4ed8;
}Performance Optimization đ
- Prefer zero-runtime styling (CSS Modules, Tailwind) over CSS-in-JS where possible to avoid client bundle overhead.
- Keep global CSS minimal â scope most styles to individual components.
- Use CSS variables for theming instead of re-rendering components with new inline styles.
- Avoid shipping unused CSS-in-JS runtime code to Server Component-heavy pages.
Best Practices â
- Choose one primary styling approach for consistency, and use others only where genuinely needed.
- Scope styles to components with CSS Modules or Tailwind rather than relying heavily on global CSS.
- Drive theming through CSS variables for easy light/dark and custom theme support.
- Keep CSS-in-JS usage confined to Client Components that need it.
Common Mistakes â ī¸
- Importing global CSS outside of the root layout, causing a build error.
- Using a CSS-in-JS library in a Server Component without the required client setup.
- Overusing !important to fight specificity issues instead of scoping styles properly.
- Loading multiple large icon or utility CSS libraries when only a handful of classes are used.