Tailwind CSS with Next.js and Styled Components: an archived setup

Tailwind CSS with Next.js and Styled Components: an archived setup cover image

Archive note (June 2021). This is a record of a Tailwind CSS 2.1.4 setup using twin.macro 2.4.2, Styled Components 5.3.0, and custom Babel configuration. It is useful when you are trying to understand an old repository, but it is not a current installation recipe. For a new app, start with the current Tailwind CSS guide for Next.js or Next.js's CSS guide.

I was trying to add utility classes to a Next.js application that already used Styled Components. twin.macro was the bridge. It let me write Tailwind-style classes inside CSS-in-JS, so I could keep the component model already in the application.

That bridge solved a real problem, but it also explains why this setup feels busy. The application needed Tailwind, a macro runner, Styled Components' Babel plugin, and a webpack workaround before a small component could render. The code below shows what each piece was doing.

The original dependency shape

The project started as an ordinary Next.js application:

yarn create next-app example
cd example

The original dependency set looked like this:

# Historical versions from the original June 2021 lockfile.
yarn add tailwindcss@2.1.4 postcss@8.3.2 autoprefixer@10.2.6 twin.macro@2.4.2 styled-components@5.3.0
yarn add -D babel-plugin-macros@3.1.0 babel-plugin-styled-components@1.12.0

These pins describe the old repository, not a supported upgrade path. I kept them so a maintainer can reproduce the same dependency boundary without accidentally resolving today's packages. A new app should use the current setup shown at the end of this article.

Connect the macro to Styled Components

Create babel-plugin-macros.config.js:

module.exports = {
  twin: {
    preset: 'styled-components',
  },
}

Next also needed a .babelrc so Babel loaded the macro and the Styled Components plugin together:

{
  "presets": ["next/babel"],
  "plugins": [
    "babel-plugin-macros",
    [
      "styled-components",
      {
        "pure": true,
        "ssr": true
      }
    ]
  ]
}

The macro configuration is the part that made the example pleasant to write. The cost was that framework upgrades now had more integration points to check.

Add the global reset

In the Pages Router, _app.js wrapped the application with GlobalStyles from twin.macro:

import React from 'react'
import { GlobalStyles } from 'twin.macro'
 
const App = ({ Component, pageProps }) => (
  <>
    <GlobalStyles />
    <Component {...pageProps} />
  </>
)
 
export default App

The old setup also generated a Tailwind configuration file:

npx tailwindcss init

Then came a webpack workaround for the fs module:

module.exports = {
  webpack: (config, { isServer }) => {
    if (!isServer) {
      config.node = { fs: 'empty' }
    }
 
    return config
  },
}

That fs: 'empty' line dates the article more than any other detail. It belongs to the old webpack configuration and should not be copied into a current Next.js application. Modern Next.js projects use the App Router's root layout for global CSS, and the current Tailwind integration uses the @tailwindcss/postcss plugin.

The component this enabled

import tw, { css } from 'twin.macro'
 
export default function Index() {
  return (
    <div
      css={[
        css`
          height: 100vh;
        `,
        tw`flex items-center justify-center bg-black text-2xl text-white`,
      ]}
    >
      Hello, World!
    </div>
  )
}

The component is deliberately tiny: one CSS-in-JS declaration for height, then the utility classes for layout, color, and type. That is the appeal of the bridge. The JSX stays readable even though Tailwind is not being imported as a normal stylesheet.

What became difficult to maintain

The maintenance problem was not the div. It was the agreement between Next's Babel pipeline, Styled Components' server-rendering behavior, the macro, and Tailwind's generated utilities. When something broke, the failure usually lived in that glue instead of the component itself.

For a small app, I would not choose this dependency chain today. I can still see one reasonable use for it: a large, existing Styled Components codebase that needs a gradual migration and has enough tests to make the boundary safe. That is a migration decision, not a reason to start a new app this way.

A pointer to the maintained current guide

I am not duplicating the current setup inside this archive. The following snippet is only a pointer to the Next.js generator and deliberately uses @latest; it is not a pinned reproduction of the current Tailwind and Next.js toolchain. Use the separately maintained current Tailwind CSS 4 guide for Next.js for the version-sensitive instructions and the files you should keep.

# Pointer only; use the maintained guide above for the current setup.
pnpm create next-app@latest my-tailwind-app --yes
cd my-tailwind-app
pnpm dev

That route puts global Tailwind CSS in the App Router stylesheet and leaves component markup with ordinary className values. The linked article walks through the manual PostCSS files and a complete starter page without making this historical post the source of truth for current versions.

I am keeping this post because old frontend notes are useful when they are honest about their date. If you inherit twin.macro, .babelrc, and config.node.fs in the same project, this is the shape you are looking at—and the reason I would plan the migration around the toolchain boundary, not around rewriting one component at a time.