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

Archive note (June 2021). This is a record of a Tailwind CSS 2.1.4 setup using
twin.macro2.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 exampleThe 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.0These 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 AppThe old setup also generated a Tailwind configuration file:
npx tailwindcss initThen 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 devThat 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.