Styling in Next.js 🎨

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

You can mix multiple styling approaches within the same project — there's no single "correct" way to style a Next.js app.

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

Global CSS can only be imported in the root layout — importing it inside a nested component will cause a build error.

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

Run npm install sass before using .scss files — no other configuration is needed.

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

Tailwind works seamlessly in both Server and Client Components, since it produces no client-side JavaScript.

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

Many popular CSS-in-JS libraries need specific configuration to work with React Server Components — check each library's Next.js compatibility notes.

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

Respect the user's OS-level preference by default using the prefers-color-scheme media query, then let them override it manually.

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.

app
globals.css
theme.css

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 âš–ī¸

ApproachScopeBest For
Global CSSentire appresets, typography, variables
CSS Modulessingle componentcomponent-specific styles
Tailwind utilitiesper-elementrapid, 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

Most CSS-in-JS libraries do not work directly in Server Components — they require a Client Component boundary.

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.

Frequently Asked Questions đŸ’Ŧ

Question

Can I use Tailwind and CSS Modules together?

Answer

Yes — many projects use Tailwind for most styling and CSS Modules for a handful of complex, component-specific styles.

Question

Do I need "use client" for Styled JSX?

Answer

Yes, in the App Router <style jsx> requires the component to be a Client Component.

Question

Which styling approach is fastest?

Answer

Zero-runtime approaches like Tailwind and CSS Modules are generally fastest, since they add no client-side JavaScript overhead.

Summary 📌

Summary

Next.js's flexibility around styling means you can pick the approach that fits your team best — just be mindful of which options work in Server Components versus which require the client runtime.