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

A blue background button with white text

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 and config().

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.

Typed Tailwind landing page

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.