Using Tailwind CSS in a Next.js project
In this post, I will share the way how I set up Tailwind in a Next.js project. Including recommended tips, extensions, and tools. Let’s find out.
Important: I’m not using Tailwind with Sass, Less, Stylus, or other preprocessors. Since Tailwind is a PostCSS plugin, I will take advantage of PostCSS plugins.
I have uploaded this example project to GitHub. It’s running on CodeSandbox too.
Information
# Formatting based on `gatsby info` command
System:
OS: macOS 11
CPU: (4) x64 Intel(R) Core(TM) i5-8210Y CPU @ 1.60GHz
Shell: 5.8 - /bin/zsh
Binaries:
Node: 12.6.0
Yarn: 1.22.4
npm: 6.14.5
npmPackages:
next: 9.5.0
tailwindcss: 1.6.1
I will try to keep npmPackages
versions up-to-date.
Installation
Install Next.js by using Create Next App.
# Using Yarn
yarn create next-app tailwind-nextjs
# Using npm
npx create-next-app tailwind-nextjs
Next.js does not include the src
folder by default. As usual, I move pages
and components
into src
for better management.
# Folder tree
tailwind-nextjs
├── .next
├── node_modules
├── public
├── src
│ ├── components
│ └── pages
├── .gitignore
├── jsconfig.json
├── package.json
└── yarn.lock
I create jsconfig.json
file to use Absolute Imports and Aliases
{
"compilerOptions": {
"baseUrl": "./src"
}
}
Continue to install Tailwind and PostCSS plugins.
# Using Yarn
yarn add tailwindcss
yarn add --dev postcss-import postcss-preset-env
# Using npm
npm install tailwindcss
npm install --save-dev postcss-import postcss-preset-env
postcss-import
and postcss-preset-env
are optional plugins. I will explain in the configuration step.
Create an index CSS file following the path ./src/styles/index.css
.
@tailwind base;
@tailwind components;
@tailwind utilities;
And a postcss.config.js
file to get Tailwind compiled on build-time.
module.exports = {
plugins: ["tailwindcss"],
}
Then override the default next/app
in ./src/pages/_app.js
file.
This makes Tailwind available to every pages or components.
import "styles/index.css"
export default function App({ Component, pageProps }) {
return <Component {...pageProps} />
}
Finally, replace ./src/pages/index.js
with below content to test if Tailwind is working
const Home = () => {
return (
<div className="p-4">
<button className="px-4 py-2 font-bold text-white bg-blue-500 rounded">
Button
</button>
</div>
)
}
export default Home
Starting development server and I have a blue button as expected
Configuration
Time to get some benefits from PostCSS plugins.
postcss-import
I usually separate CSS files into chunks, like components. Then import them into the index file as the main entry-point.
@import "./fontface.css";
@tailwind base;
/* Adding style to override `base` */
@import "./custom-base.css";
@tailwind components;
/* Extracted components */
@import "./button.css";
@tailwind utilities;
There are several issues with this configuration.
The custom-base.css
will not work. At build-time, all the @import
statements will move to the top of the code. This behavior is like Hoisting in JavaScript.
So the result in HTML output looks like this:
<style>
{/* Content of fontface.css */}
</style>
<style>
{/* Content of custom-base.css */}
</style>
<style>
{/* Content of button.css */}
</style>
<style>
{/* Content of Tailwind */}
</style>
One issue is the browser will make one network request for each single separated file. It costs 4 network requests with the current configuration. It’s not a good practice.
To deal with these issues, I use postcss-import
to inline @import
rules content.
First, include postcss-import
to postcss.config.js
plugins array.
module.exports = {
plugins: [
// Should use as the first plugin of the list
"postcss-import",
"tailwindcss",
],
}
Then, change the content of the index.css
file.
@import "./fontface.css";
@import "tailwindcss/base";
@import "./custom-base.css";
@import "tailwindcss/components";
@import "./button.css";
@import "tailwindcss/utilities";
I change @tailwind
rules to @import
because @import
must precede all other statements. Otherwise, postcss-import
will ignore button.css
file.
postcss-import
is smart enough to look into the root directory or node_modules
folder. So it knows where tailwindcss
lives, I don’t have to provide the entire path.
By inline all the content into one file, it costs only one network request in the browser.
postcss-preset-env
I used to love working with styled-components
. Because it supports scss-like syntax for nesting styles. It also adds vendor prefixes.
With postcss-preset-env
, I can have the same experience.
It’s nice that postcss-preset-env
includes CSS variables, nestings, and autoprefixer by default.
module.exports = {
plugins: [
// Should use as the first plugin of the list
"postcss-import",
"tailwindcss",
/**
* Stage 0: Aspirational - This is a crazy idea.
* Stage 1: Experimental - This idea might not be crazy.
* Stage 2: Allowable - This idea is not crazy. (Default)
* Stage 3: Embraced - This idea is becoming part of the web.
* Stage 4: Standardized - This idea is part of the web.
* https://cssdb.org/#staging-process
*/
["postcss-preset-env", { stage: 1 }],
// Alternative to `postcss-preset-env`
// "postcss-nesting",
// "autoprefixer",
],
}
Now I can apply some sugar-syntax into my CSS files
button {
&.btn {
/* styles for button.btn */
&.btn-primary {
/* styles for button.btn.btn-primary */
}
&:hover {
/* styles for button.btn:hover */
}
}
}
Optimization
Creating an optimized production build, by running yarn build
.
The generated CSS file size is too heavy to serve on production.
/tailwind-nextjs/.next/static/css/1393d4dcf2c9c7cf7cb2.css
+--------------------------------------------------------------+
| Size | 823.72 KiB |
|--------------------------------------------------------------|
| Gzipped | 104.79 KiB |
|--------------------------------------------------------------|
| Mime type | text/css |
+--------------------------------------------------------------+
There are several strategies for keeping generated CSS small and performant.
I can remove unused CSS in the project by using Purgecss.
Because Tailwind has built-in PurgeCSS, simply add an option in tailwind.config.js
module.exports = {
purge: ["./src/**/*.js"],
theme: {},
variants: {},
plugins: [],
}
Run yarn build
again, and I have this
/tailwind-nextjs/.next/static/css/ff9f29072f7e88457366.css
+--------------------------------------------------------------+
| Size | 991 bytes |
|--------------------------------------------------------------|
| Gzipped | 522 bytes |
|--------------------------------------------------------------|
| Mime type | text/css |
+--------------------------------------------------------------+
There must be something wrong, it’s too small. When viewing the output content, I find out that it does not include the content of base
and components
.
To fix that, I add them to the whitelist range by a special comment.
/* purgecss start ignore */
@import "tailwindcss/base";
@import "tailwindcss/components";
/* purgecss end ignore */
@import "tailwindcss/utilities";
Now it seems more reasonable.
/tailwind-nextjs/.next/static/css/7093c40c8c97f0c3d0b5.css
+--------------------------------------------------------------+
| Size | 3.7 KiB |
|--------------------------------------------------------------|
| Gzipped | 1.41 KiB |
|--------------------------------------------------------------|
| Mime type | text/css |
+--------------------------------------------------------------+
There are more advanced strategies to add up, but it depends on how I configure Tailwind in the project.
Recommendation
After a few months of working with Tailwind, I have some tips for improving the working experience.
Unknown at-rule
By default, VSCode will make a warning on Tailwind directives. It might be annoying.
To turn this off, I go to VSCode Settings, search "Unknown at-rule" and choose to ignore
the using linter.
Tailwind CSS Intellisense
Tailwind class name completion — VSCode extension by Brad Cornes.
Feature includes:
className
suggestion and preview base on Tailwind configuration in the project.- Syntax highlight and suggestion for Tailwind directives like
@apply
,@screen
andconfig()
.
I don’t need to remember what Tailwind configuration contains anymore.
Headwind
An opinionated class sorter for Tailwind — VSCode extension by Ryan Heybourn.
It enforces consistent ordering of classes. Like Prettier for Tailwind.
I like to keep "headwind.runOnSave": true
on by default.
PostCSS Language Support
Syntax highlighting for modern and experimental CSS in VSCode — VSCode extension by csstools.
It is perfect when using PostCSS plugins with the nesting feature.
Typed Tailwind
If I use TypeScript, Typed Tailwind would be on my packages list to use with.
It brings types to Tailwind by generating TypeScript classes from Tailwind configuration.
Project Template
Another project template for Tailwind with Next.js that I’m interested in is from nhducit. It already includes a full set example using TypeScript and many other features.