Getting Started with Tailwind CSS in Next.js
Introduction
Tailwind CSS is a utility-first CSS framework that lets you build custom designs directly in your markup, without writing a single line of custom CSS. Instead of predefined component classes like Bootstrap's btn or card, Tailwind gives you low-level utility classes, like flex, pt-4, text-center, that you compose to build any design you want.
The result is a faster development workflow, smaller production CSS bundles, and no context-switching between your HTML and a stylesheet.
In this guide, you'll learn how to set up Tailwind CSS v4 in a Next.js 15 project, understand how its core concepts work, common patterns, and best practices to follow.
This guide covers Tailwind CSS v4, released in early 2025. If you're on an older project using v3, the setup steps are different. Check the Tailwind v3 docs for that.
Setting up Tailwind CSS in Next.js
There are two ways to get Tailwind running in a Next.js project: letting create-next-app handle it automatically, or adding it manually to an existing project.
Option 1 - New project with create-next-app
The easiest way to start is with the official Next.js scaffolding tool. Run the following command and answer Yes when it asks about Tailwind CSS:
npx create-next-app@latest my-app
In the setup prompts, answer Yes when asked if you want to use Tailwind CSS:
Would you like to use Tailwind CSS? › Yes
Then the CLI automatically installs Tailwind and configures everything for you.
Option 2 - Adding Tailwind to an existing project
If you're adding Tailwind to an existing Next.js project, install it along with the PostCSS plugin:
npm install tailwindcss @tailwindcss/postcss postcss
Next, create a postcss.config.js file in the root of your project:
// postcss.config.js
const config = {
plugins: {
'@tailwindcss/postcss': {},
},
};
export default config;
Finally, open your global stylesheet (app/globals.css or styles/globals.css) and replace its contents with a single import:
@import "tailwindcss";
That single line replaces the three directives (
@tailwind base,@tailwind components,@tailwind utilities) used in Tailwind v3. Tailwind v4 automatically detects your project files, so there's nocontentarray to configure.
Restart your dev server and Tailwind is ready to use.
How Tailwind works
Tailwind scans your project files for utility class names, generates only the CSS those classes need, and injects it into your stylesheet. In production, this means your CSS bundle contains only the styles you actually use, often just a few kilobytes.
Here's a quick example of Tailwind in action. In app/page.tsx, replace the contents with:
export default function Home() {
return (
<main className="min-h-screen bg-gray-950 flex items-center justify-center">
<div className="bg-emerald-700 text-white rounded-xl shadow-lg p-8 w-64 text-center">
<h1 className="text-2xl font-bold mb-2">Hello, Tailwind!</h1>
<p className="text-emerald-100 text-sm">Styled without a single line of CSS.</p>
</div>
</main>
);
}
Run npm run dev and visit http://localhost:3000. You'll see a centered card with a dark background, built entirely with utility classes.
Directives
Tailwind provides a set of custom at-rules called directives that give you control over how styles are generated and applied.
@import
The @import "tailwindcss" directive is the entry point for Tailwind v4. It injects Tailwind's base reset, component layer, and utility classes into your stylesheet.
@import "tailwindcss";
@apply
@apply lets you use Tailwind utility classes inside custom CSS rules. This is useful when you want to keep your markup clean but still use Tailwind under the hood:
.btn-primary {
@apply px-4 py-2 bg-emerald-600 text-white rounded-lg font-medium;
@apply hover:bg-emerald-700 transition-colors duration-200;
}
Use @apply sparingly. If you find yourself reaching for it often, it might be a sign to extract a reusable component instead.
@layer
@layer tells Tailwind which layer your custom styles belong to: base, components, or utilities. This controls when your styles are applied relative to Tailwind's own styles.
@layer base {
h1 {
@apply text-3xl font-bold tracking-tight;
}
a {
@apply text-emerald-500 hover:underline;
}
}
@layer components {
.card {
@apply bg-white rounded-xl shadow-md p-6;
}
}
@utility
@utility is new in Tailwind v4. It's the recommended way to define custom utility classes that work just like Tailwind's built-in ones, including support for variants like hover:, md:, and dark::
@utility flex-center {
display: flex;
justify-content: center;
align-items: center;
}
@utility text-balance {
text-wrap: balance;
}
Once defined, you can use these utilities exactly like any other Tailwind class:
<div className="flex-center min-h-screen">
<h1 className="text-balance text-4xl font-bold">Hello World</h1>
</div>
Theme customization
In Tailwind v4, theme customization happens in your CSS file using the @theme directive, not in a JavaScript config file. This is one of the biggest changes from v3.
What are theme variables?
Theme variables are CSS custom properties defined inside an @theme block. They serve two purposes at once: they generate Tailwind utility classes, and they're available as regular CSS variables anywhere in your stylesheet.
@import "tailwindcss";
@theme {
--color-primary: #a6dbc8;
--font-sans: 'Inter', system-ui, sans-serif;
--breakpoint-md: 768px;
}
After defining --color-primary, Tailwind generates the full set of color utilities: bg-primary, text-primary, border-primary, ring-primary, fill-primary, stroke-primary, and so on. The same variable is also available as var(--color-primary) in your own CSS.
Why @theme and not :root?
You might wonder why Tailwind uses a special directive instead of just :root. The reason is that :root declarations are passive, they're just CSS variables with no special behavior. @theme tells Tailwind to actively process the variables and generate corresponding utility classes.
When Tailwind compiles your CSS, it reads the @theme block, understands the namespace of each variable (color, font, spacing, etc.), and generates the right utilities for each one. Variables in :root are invisible to this process.
Theme variable namespaces
The variable namespace determines which utility classes get generated. Here's a reference for the most common ones:
| Namespace | Example variable | Generated utilities |
|---|---|---|
--color-* | --color-mint | bg-mint, text-mint, border-mint |
--font-* | --font-sans | font-sans |
--text-* | --text-lg | text-lg |
--font-weight-* | --font-weight-bold | font-bold |
--tracking-* | --tracking-wide | tracking-wide |
--leading-* | --leading-relaxed | leading-relaxed |
--breakpoint-* | --breakpoint-tablet | tablet: prefix |
--spacing-* | --spacing-18 | p-18, m-18, w-18, gap-18 |
--radius-* | --radius-card | rounded-card |
--shadow-* | --shadow-card | shadow-card |
--animate-* | --animate-spin | animate-spin |
--ease-* | --ease-in | ease-in |
--width-* | --width-sidebar | w-sidebar |
Extending the default theme
By default, your @theme block adds to Tailwind's built-in theme. You don't need to redefine anything that already exists:
@import "tailwindcss";
@theme {
/* Add a new brand color alongside Tailwind's existing colors */
--color-mint: #a6dbc8;
--color-mint-dark: #6fbfa8;
/* Add a custom font */
--font-sans: 'Inter', system-ui, sans-serif;
--font-mono: 'JetBrains Mono', monospace;
/* Add a custom breakpoint */
--breakpoint-xs: 480px;
/* Add a custom border radius */
--radius-card: 0.75rem;
}
You can now use bg-mint, xs:text-sm, rounded-card, and font-mono alongside Tailwind's existing classes like bg-blue-500 or text-lg.
Overriding default values
To override a specific default value, define a variable with the same name:
@theme {
/* Override Tailwind's default sans font */
--font-sans: 'Inter', system-ui, sans-serif;
/* Override the default xl breakpoint */
--breakpoint-xl: 1400px;
}
Replacing the entire theme
To replace Tailwind's defaults completely and start from scratch, set --*: initial before your declarations:
@theme {
--*: initial;
--color-white: #fff;
--color-black: #000;
--color-brand: #a6dbc8;
--font-sans: 'Inter', system-ui, sans-serif;
--breakpoint-sm: 640px;
--breakpoint-md: 768px;
--breakpoint-lg: 1024px;
}
This removes all of Tailwind's built-in colors, fonts, spacing, and breakpoints. Use this when you want full control over the design system with no defaults leaking in.
To disable only one namespace, target it specifically:
@theme {
/* Remove all default colors, keep everything else */
--color-*: initial;
--color-brand: #a6dbc8;
--color-danger: #f87171;
}
Custom animations
You can define custom keyframe animations directly inside @theme. Tailwind will generate the animate-* utility and inject the keyframes into your stylesheet:
@theme {
--animate-fade-in: fade-in 0.3s ease-out;
@keyframes fade-in {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
}
Then use it like any other animation:
<div className="animate-fade-in">Hello</div>
@theme inline
Sometimes you want a CSS variable that references another CSS variable, rather than a static value. This is common when building dark mode or theming systems. Use @theme inline for this:
@import "tailwindcss";
@theme {
--color-primary: #a6dbc8;
--color-secondary: #6fbfa8;
}
@theme inline {
--color-surface: var(--color-primary);
}
Without inline, Tailwind would try to resolve var(--color-primary) at build time and fail. With inline, the variable stays as a live reference in the output, updating dynamically at runtime.
This is the right tool for semantic color tokens that change between light and dark mode:
@theme inline {
--color-text: var(--color-gray-900);
--color-bg: var(--color-white);
}
@media (prefers-color-scheme: dark) {
:root {
--color-gray-900: #f5f5f5;
--color-white: #0f0f0f;
}
}
Sharing theme variables across projects
If you work with multiple packages or apps that share a design system, you can extract your theme into a standalone CSS file and import it:
/* packages/design-tokens/theme.css */
@theme {
--color-brand: #a6dbc8;
--color-brand-dark: #6fbfa8;
--font-sans: 'Inter', system-ui, sans-serif;
--radius-card: 0.75rem;
}
/* apps/web/app/globals.css */
@import "tailwindcss";
@import "@your-org/design-tokens/theme.css";
Both apps get the same theme variables and the same generated utility classes.
Using theme variables in custom CSS
Because @theme variables are real CSS custom properties, you can use them anywhere:
@layer components {
.card {
background: var(--color-surface);
border-radius: var(--radius-card);
padding: var(--spacing-6);
box-shadow: var(--shadow-md);
}
}
You can also reference them in arbitrary values inside your JSX:
<div className="bg-[var(--color-brand)] rounded-[var(--radius-card)]">
Custom token, Tailwind syntax
</div>
Using theme variables in JavaScript
To read theme values in JavaScript (for charting libraries, canvas, or animation), use getComputedStyle:
const styles = getComputedStyle(document.documentElement);
const brandColor = styles.getPropertyValue('--color-brand').trim();
const sans = styles.getPropertyValue('--font-sans').trim();
This always reflects the current computed value, including any runtime overrides from dark mode toggles or media queries.
If you need a JavaScript config file for tooling or IDE support, you can still create a tailwind.config.ts file:
import type { Config } from 'tailwindcss';
const config: Config = {
theme: {
extend: {
colors: {
primary: '#a6dbc8',
},
},
},
};
export default config;
Responsive design
Tailwind uses a mobile-first approach to responsive design. You add breakpoint prefixes to any utility class to apply it at a specific screen width and above.
The default breakpoints are:
| Prefix | Minimum width |
|---|---|
sm | 640px |
md | 768px |
lg | 1024px |
xl | 1280px |
2xl | 1536px |
<div className="flex flex-col md:flex-row gap-4">
<aside className="w-full md:w-64 bg-gray-800 p-4 rounded-lg">
Sidebar
</aside>
<main className="flex-1 bg-gray-900 p-6 rounded-lg">
Main content
</main>
</div>
In this example, the layout stacks vertically on mobile and switches to a side-by-side row on medium screens and above.
To add custom breakpoints in v4, define them in your @theme block:
@theme {
--breakpoint-tablet: 900px;
}
Then use tablet: as a prefix in your markup:
<h1 className="text-2xl tablet:text-5xl font-bold">Hello</h1>
Dark mode
Tailwind supports dark mode with the dark: variant. By default, it follows the user's system preference via the prefers-color-scheme media query.
<div className="bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 p-6 rounded-xl">
<h2 className="text-xl font-semibold">This card adapts to dark mode</h2>
<p className="text-gray-600 dark:text-gray-400 mt-2">
The colors change automatically based on the user's system theme.
</p>
</div>
If you want to control dark mode manually (for example, with a toggle button), configure it in your CSS:
@import "tailwindcss";
@variant dark (&:where(.dark, .dark *));
Then toggle the dark class on your <html> element using JavaScript:
document.documentElement.classList.toggle('dark');
Arbitrary values
Sometimes you need a specific value that doesn't exist in Tailwind's default scale. Rather than reaching for custom CSS, you can use arbitrary values with square bracket notation:
<div className="w-[340px] h-[200px] bg-[#1a1a2e] rounded-[10px]">
<p className="text-[15px] leading-[1.8] text-[#e0e0ff]">
Custom sizing without leaving your markup.
</p>
</div>
Arbitrary values work with any utility in Tailwind, including spacing, colors, font sizes, grid columns, and more:
<div className="grid grid-cols-[1fr_320px] gap-[48px]">
<main>Content</main>
<aside>Sidebar</aside>
</div>
Composing components
One of the strongest arguments for Tailwind is how it handles component extraction. Instead of writing CSS, you extract reusable logic into a component.
Here's the pattern: start with inline classes, and when you find yourself copying them, pull them into a component:
// Before: repeated markup
<button className="px-4 py-2 bg-emerald-600 text-white rounded-lg font-medium hover:bg-emerald-700 transition-colors">
Save
</button>
<button className="px-4 py-2 bg-emerald-600 text-white rounded-lg font-medium hover:bg-emerald-700 transition-colors">
Submit
</button>
// After: extracted component
const Button = ({ children }: { children: React.ReactNode }) => (
<button className="px-4 py-2 bg-emerald-600 text-white rounded-lg font-medium hover:bg-emerald-700 transition-colors">
{children}
</button>
);
This keeps Tailwind classes co-located with the component they style, which is easier to maintain than a separate CSS file.
For more complex components with conditional styles, use the clsx or cva libraries:
npm install clsx class-variance-authority
import { cva } from 'class-variance-authority';
const button = cva(
'px-4 py-2 rounded-lg font-medium transition-colors',
{
variants: {
intent: {
primary: 'bg-emerald-600 text-white hover:bg-emerald-700',
secondary: 'bg-gray-800 text-gray-100 hover:bg-gray-700',
danger: 'bg-red-600 text-white hover:bg-red-700',
},
size: {
sm: 'text-sm px-3 py-1.5',
md: 'text-base px-4 py-2',
lg: 'text-lg px-6 py-3',
},
},
defaultVariants: {
intent: 'primary',
size: 'md',
},
}
);
<button className={button({ intent: 'secondary', size: 'lg' })}>
Click me
</button>
Class sorting with Prettier
One practical problem with utility-first CSS is that class lists can get long and inconsistent. Different developers order classes differently, which makes diffs noisy and code harder to scan.
Tailwind maintains an official Prettier plugin that automatically sorts your classes following Tailwind's recommended class order. It runs on save, works with custom Tailwind configurations, and since it's just a Prettier plugin, it works in every editor that supports Prettier.
Install it alongside Prettier:
npm install -D prettier prettier-plugin-tailwindcss
Then add it to your prettier.config.js:
// prettier.config.js
export default {
plugins: ['prettier-plugin-tailwindcss'],
};
Here's what the sorting looks like in practice:
<!-- Before -->
<button class="text-white px-4 sm:px-8 py-2 sm:py-3 bg-sky-700 hover:bg-sky-800">
Submit
</button>
<!-- After -->
<button class="bg-sky-700 px-4 py-2 text-white hover:bg-sky-800 sm:px-8 sm:py-3">
Submit
</button>
The plugin groups classes by category (layout, spacing, typography, background, interactivity) and puts responsive variants at the end. Once you have it running, you stop thinking about class order entirely.
Conclusion
Tailwind CSS v4 is the most productive way to style a Next.js application today. The setup is minimal, the utility classes are predictable, and the CSS output is lean. Once you're past the initial learning curve of memorizing class names, which happens faster than you'd expect, you'll find it hard to go back to writing plain CSS or using component libraries.
The concepts covered here are enough to build real production UIs. As you grow more comfortable, explore the Tailwind CSS documentation for advanced features like container queries, 3D transforms, and the full list of available utilities.