Skip to main content
ASoc
Tutorial

React + Tailwind CSS: The Config File Every Setup Guide Starts With Is Gone

v4 deleted the init command, the JS config and the content globs. What a real React setup is instead: one import, one PostCSS plugin, 879 class attributes, 133 lines of CSS.

The ASoc Team8 min read

Setting up Tailwind CSS in a React project takes one import line and one PostCSS plugin. There is no tailwind.config.js, no init command, and no content array — Tailwind v4 removed all three. This codebase runs 92 components and 879 class attributes on exactly one hand-written stylesheet, 133 lines long.

The setup guides are describing a version that no longer ships

Search for React and Tailwind and the top results all open the same way: install tailwindcss, postcss and autoprefixer, run npx tailwindcss init -p, then add a content glob so the compiler knows which files to scan. That was correct for v3. It is not what a v4 install looks like, and the difference is not cosmetic — two of those steps cannot be performed at all.

The tailwindcss package installed here is 4.3.1, and its package.json declares no bin field. There is no tailwindcss executable in node_modules/.bin, so npx tailwindcss init has nothing to run. v4 moved the CLI into a separate @tailwindcss/cli package that a PostCSS-based setup never installs. The five @tailwindcss/* packages actually present here are postcss, node, and three oxide builds — the Rust engine that replaced the JavaScript one.

What the guides teachWhat v4 actually needs here
npm i -D tailwindcss postcss autoprefixertailwindcss + @tailwindcss/postcss (autoprefixer is built in)
npx tailwindcss init -pNothing — the command does not exist in this package
tailwind.config.js with a theme.extend blockA @theme block in your CSS
content: ["./src/**/*.{js,jsx,ts,tsx}"]Nothing — source detection is automatic
@tailwind base; @tailwind components; @tailwind utilities;One line: @import "tailwindcss";
A postcss.config.js naming three pluginsThree lines naming one

Both of the deleted steps existed to tell the compiler things it now works out on its own. That is the whole story of the v4 setup: the configuration surface shrank to a stylesheet.

Every configuration file in this project, quoted whole

There are two, and one of them is three lines. postcss.config.mjs:

const config = {
  plugins: {
    "@tailwindcss/postcss": {},
  },
};

export default config;

That is the entire build wiring. The second file is src/app/globals.css, which opens like this:

@import "tailwindcss";

/* Class-based dark mode (toggled by adding `dark` to <html>) */
@custom-variant dark (&:where(.dark, .dark *));

@theme {
  --font-sans: var(--font-outfit), ui-sans-serif, system-ui, sans-serif;

  /* Brand / primary scale */
  --color-primary: #465fff;
  --color-primary-25: #f2f7ff;
  --color-primary-50: #ecf3ff;
  /* … */
}

The @theme block is where a tailwind.config.js would have gone. It declares 41 tokens, and each one generates utilities: --color-primary-500 is what makes bg-primary-500 a real class. The mechanics of that mapping — and the 22 places this codebase spelled a value literally instead of using the token that already existed — are audited in Tailwind design tokens; the point here is only that the file is CSS, not JavaScript, and there is no second config to keep in sync with it.

Find every stylesheet in the repository and you get one result: src/app/globals.css, 133 lines. Against that sits 879 className attributes across 139 .tsx files — 716 of them in the 92 components under src/components, the rest in route files under src/app.

The React-specific part: Tailwind is a build step, not a runtime

This is the property that decides how Tailwind behaves in a modern React app, and no install guide mentions it. package.json lists 13 runtime dependencies and not one of them is a styling library. tailwindcss is a devDependency, because by the time the browser sees the page there is no Tailwind left — only a stylesheet.

That matters because of where React draws its boundary now. A Server Component renders on the server and ships no client JavaScript of its own; a Client Component opts in with "use client". Twenty-four of this codebase's 92 components are Client Components — a ratio that adopting shadcn/ui would move, since its interactive components declare "use client" themselves. Tailwind is indifferent to which side a component is on, because it never runs at either — the classes were resolved at build time. CSS-in-JS cannot make that claim: a styled-components or Emotion component needs a runtime, which forces a client boundary. That ranking of the four styling options by the Server Components constraint is the subject of styling React components, and this is the one line of it worth repeating in a setup post: Tailwind is the only one of the four that costs nothing to use in a Server Component.

The font shows the same seam from the other direction. src/app/layout.tsx loads Outfit through next/font:

const outfit = Outfit({
  variable: "--font-outfit",
  subsets: ["latin"],
});

and globals.css points --font-sans at that variable, so font-sans in any component resolves to a self-hosted font file. Neither half knows about the other; the CSS variable is the entire contract.

The rule that actually breaks React code

Tailwind finds classes by scanning your source for literal strings. It does not evaluate your components. So a class name assembled from a variable does not exist as far as the compiler is concerned, and the utility is never generated:

// Broken: no such literal string appears in the source.
<div className={`bg-primary-${shade}`} />

// Works: both literals are present for the scanner to find.
<div className={shade === 500 ? "bg-primary-500" : "bg-primary-600"} />

This is the single most common way a React + Tailwind setup appears to be broken when it is working exactly as designed, and it is why this codebase reaches for an inline style in the handful of places where a value genuinely cannot be enumerated ahead of time — an accordion's measured row height, for instance. The full census of those cases is in styling React components; what a class is at the compiler level is in what is a Tailwind class.

The defect this codebase hit: @theme edits that silently do nothing

Tailwind v4 under Turbopack does not hot-reload changes to the @theme block. Add --color-brand-500 to globals.css while npm run dev is running, use bg-brand-500 in a component, and the class silently fails to generate — no error, no warning, just an element with no background. The dev server is still serving utilities compiled from the stylesheet as it looked at boot.

The fix is to restart npm run dev after editing a token. It is written into this repository's CLAUDE.md as a standing instruction, because it cost real debugging time before anyone recognised the pattern: it looks exactly like a typo in a class name, so the first instinct is to go and check the class name.

One more piece of tooling exists for a reason worth naming. .prettierrc.json is four lines:

{
  "plugins": ["prettier-plugin-tailwindcss"],
  "tailwindStylesheet": "./src/app/globals.css"
}

The plugin sorts class strings into a canonical order on save. In a codebase with 879 class attributes, hand-sorting produces diff noise and duplicate utilities nobody notices; tailwindStylesheet is the v4-specific key that tells the plugin where the theme lives, since there is no config file for it to read.

Mistakes and how they show up

MistakeWhat you seeFix
Following a v3 guide on a v4 installnpx tailwindcss init errors — no such commandThere is no CLI in the tailwindcss package; write the theme in CSS
Keeping @tailwind base/components/utilitiesBuild error or no styles at allReplace all three with @import "tailwindcss";
Adding a content globNothing breaks, nothing helpsSource detection is automatic in v4; delete it
Building a class name from a variableThe element renders unstyledWrite both literals out, or use an inline style for computed values
Editing @theme with the dev server runningNew utilities silently do not generateRestart npm run dev
Installing autoprefixer alongside v4Redundant work in the pipeline@tailwindcss/postcss already handles it

Frequently asked questions

Do I still need a tailwind.config.js file with React? No. v4 reads its configuration from a @theme block in your CSS. A JS config can still be loaded explicitly with the @config directive if you are migrating an old project, but a new setup does not need one — this codebase has zero config files for Tailwind and 41 theme tokens.

Does Tailwind work in React Server Components? Yes, identically to Client Components, because Tailwind compiles to a stylesheet at build time and ships no runtime. This is the practical advantage over CSS-in-JS libraries, which need a runtime and therefore force a "use client" boundary on any component that uses them.

Why isn't my Tailwind class working in React? Almost always because the class name was constructed from a variable. Tailwind scans source files for literal strings and never executes your code, so an interpolated class name is invisible to it. Write out the complete class names you want and switch between them, rather than assembling one.

Is Create React App still the right way to start a React and Tailwind project? CRA is deprecated, and most of the guides pairing it with Tailwind date from the v3 era, which is why they teach a setup that no longer applies. A Vite or Next.js project with @tailwindcss/postcss is the current path. This codebase runs Next.js 16.2.9 and React 19.2.4.

Templates this setup ships in

ASoc Ally is an AI support-chatbot marketing site, ASoc Amplify is a social-media-management SaaS site, and ASoc Atelier is a designer's portfolio and studio site — each one built on exactly the configuration above: one @import, one @theme block, no JavaScript config file anywhere.

Browse the full sets: Next.js landing page templates, Tailwind landing page templates. For the upgrade path from a v3 project, read the Tailwind v4 migration guide; for what the compiler is actually doing with those class strings, read what is a Tailwind class.

Keep reading

Tutorial9 min read

Robots.txt and Sitemap.xml in Next.js: One Declared Date, 420 Routes

The STOREFRONT_COPY_REVISED fix for a sitemap that told Google 111 pages changed on a date nothing did, plus the noindex-vs-disallow split this codebase actually uses.

Read more