Tailwind CSS not working after Vite build: dev vs dist
Find out in two commands whether the built CSS really lacks your classes or the browser never loaded it, then apply the one change that fixes each case.
A Stack Overflow question, Why is tailwindcss not working when building my vite project, describes a vanilla-ts Vite project with Tailwind installed through PostCSS: npm run dev "works flawlessly", but after npm run build "tailwind is not being applied". The question has no accepted answer, and the only answer fixes a different symptom (a content list missing .vue files, which breaks dev too). This page reproduces the setup with the package versions that were current when the question was asked, and separates the two situations that really produce "fine in dev, unstyled after build".
TL;DR#
First check whether the built CSS file contains your classes. With Tailwind CSS v3 and the question's exact config, it does: the dev server and vite build ship the same utilities. If your file contains them too, check how the build is viewed. dist/index.html loads /assets/index.<hash>.css, and a page opened straight from disk resolves that to the root of the file system. Serve the folder with npm run preview instead. With the Tailwind CSS v2 classic engine and an array purge configuration, purging defaults to production only, so a purge list that skips the root index.html removes those classes from the build only. Add './index.html' to the list.
What the build actually contains#
The reproduction ran in a throwaway node:18-alpine container on 20 October 2026, with Vite 2.9.14 and Tailwind CSS 3.1.5 (the releases npm served in the days before the question was posted on 11 July 2022). It used the question's postcss.config.js, tailwind.config.js and style.css as written, and a main.ts that injects <h1 class="text-3xl font-bold underline"> so there is a utility to look for. The dev server's stylesheet and the built file were both searched for the utilities and for Preflight's box-sizing reset:
GET http://127.0.0.1:5173/src/style.css -> 200 bytes 10327
contains ".text-3xl" : true
contains ".underline" : true
contains "box-sizing: border-box" : truevite v2.9.14 building for production...
dist/assets/index.e52f266b.css 4.20 KiB / gzip: 1.33 KiB
dist/assets/index.e52f266b.css contains .text-3xl : true
dist/assets/index.e52f266b.css contains .underline : true
dist/assets/index.e52f266b.css contains box-sizing:border-box : trueSo with this config, Tailwind v3 does not drop anything at build time. Starting the dev server with NODE_ENV=production also served both utilities, which rules out an environment switch inside Tailwind v3. If your own build file does lack classes, compare it with the dev output in the same way before changing any config. A class missing in both is a content problem, the case covered for Next.js in Tailwind classes compiling to nothing. The mechanism is the same in Vite.
Why the build looks unstyled: absolute asset paths#
Here is the dist/index.html that vite build wrote:
<script type="module" crossorigin src="/assets/index.2cd80f67.js"></script>
<link rel="stylesheet" href="/assets/index.e52f266b.css">Both paths start with /, because Vite's base option defaults to /. On a web server, /assets/... means "from the site root", which is correct. Opened from disk by double-clicking, the same href is resolved against a file:// URL, and the root is the root of the file system, outside your project:
--- what that href resolves to when dist/index.html is opened from disk
file:///assets/index.e52f266b.cssThe reproduction computed this resolved URL; it did not open the page in a browser. What follows is what that URL and MDN imply. No file exists at that path, so the stylesheet cannot load: no utilities and no Preflight. The script tag would fail too, for a second reason. The entry is a module script, and MDN's JavaScript modules guide warns that if you load the HTML file locally with a file:// URL, "you'll run into CORS errors due to JavaScript module security requirements". In the question's project, main.ts writes the page content with innerHTML, so a build opened from disk would show neither the styles nor the text.
The question does not say how the build was viewed, so this is not a diagnosis of that particular machine. It is the one cause reproduced here that is consistent with the config given: identical config, styles in dev, none after the build.
Fix: serve dist instead of opening it#
- Build as usual:
npm run build- Serve the output with the
previewscript that thevanilla-tstemplate'spackage.jsonalready declares:
npm run preview- Open the URL it prints. In the reproduction, the preview server returned the built stylesheet with every utility in it:
> Local: http://127.0.0.1:4173/
GET http://127.0.0.1:4173/assets/index.e52f266b.css -> 200 bytes 4298
contains ".text-3xl" : true
contains ".underline" : trueThe Vite static deploy guide describes vite preview as "an easy way to check if the production build looks OK in your local environment", and adds that it is "not meant as a production server". For a real deployment, upload dist to any static host. If you build it inside a Docker image and Vite then cannot find its entry, check whether a .dockerignore rule excluded index.html from the build context, as shown in the Vite entry module and .dockerignore fix.
Deploying under a sub-path: set base#
The same absolute paths break a deployment that does not live at the domain root, such as https://example.eu/app/ or a GitHub Pages project site. The browser requests /assets/... from the domain root and gets a 404. The Vite build guide says to set the base option when "deploying your project under a nested public path", for example in vite.config.ts:
import { defineConfig } from 'vite'
export default defineConfig({
base: '/app/',
})base also accepts ./, which the docs list "for embedded deployment". Building with it rewrites both tags to relative paths:
<script type="module" crossorigin src="./assets/index.2cd80f67.js"></script>
<link rel="stylesheet" href="./assets/index.e52f266b.css">That lets a page opened from disk find its CSS, but it does not lift the module-script restriction quoted above. Use it for hosts that serve the app from an unknown folder, not as a way to skip the preview server.
Tailwind v2 classic: production purge can expose a missing path#
The earlier question Vite Vanilla build with Tailwind doesn't work has the same symptom with a different cause. Its config uses the Tailwind CSS v2 classic engine and an array purge configuration with only ./src/**/*.html and ./src/**/*.js. For that configuration, purgeUnusedStyles.js uses this production condition as its default:
config.purge !== false && config.purge !== undefined && process.env.NODE_ENV === 'production'The Vite modes documentation shows that vite build sets NODE_ENV to "production". The dev server leaves purging off, as the 4 MB dev stylesheet below shows. So dev serves the whole framework and the build keeps only what the purge paths mention. Vite keeps index.html at the project root, outside ./src. Reproduced with Vite 2.9.14 and Tailwind CSS 2.2.19, with the classes written in the root index.html:
GET http://127.0.0.1:5175/src/style.css -> 200 bytes 4063490
contains ".text-3xl" : true
contains ".underline" : true--- npm run build (purge list = ./src/**/*.html, ./src/**/*.js)
dist/assets/index.95095f02.css 3.16 KiB / gzip: 1.28 KiB
dist/assets/index.95095f02.css contains .text-3xl : false
dist/assets/index.95095f02.css contains .underline : falseThe single change was adding the root file to the list:
module.exports = {
purge: [
'./index.html',
'./src/**/*.html',
'./src/**/*.js',
],
darkMode: false,
theme: {
extend: {},
},
variants: {
extend: {},
},
plugins: [],
}The next build kept both utilities:
dist/assets/index.2de594e9.css 3.32 KiB / gzip: 1.34 KiB
dist/assets/index.2de594e9.css contains .text-3xl : true
dist/assets/index.2de594e9.css contains .underline : trueThe production condition is a default, not an invariant of every v2 setup. The same function gives an explicit purge.enabled value precedence, as the manual activation documentation explains. A supplementary Docker reproduction on 4 November 2026 with Tailwind 2.2.19 confirmed that purge.enabled: true removes .underline in development when the root HTML is excluded; adding only ./index.html restores it. The v2 JIT engine also generates utilities from source paths in development: the same missing-path and corrected-path controls omitted and restored .underline there too.
The dev-only gap above therefore applies to the reproduced classic configuration. In v3 the content list is read by the dev server too, so a missing path leaves the class out of both commands, and the only answer on the question shows Tailwind printing its warning during npm run dev.
Verify the fix#
Check the file before you check the browser. After npm run build, search the CSS for a utility you know the page uses:
grep -o 'text-3xl' dist/assets/*.cssNo match means the build really lacks the class: fix the content list (v3) or purge list (v2), and remember that the root index.html is a template too. A match means the CSS is fine and the problem is loading it. Run npm run preview, open the printed URL, and look at the stylesheet request in the browser's network panel. On a deployed site, a 404 on /assets/... points to base.
One email a month — the latest explained errors
What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.
Following these steps today installs Tailwind v4#
The question's setup (npm install -D tailwindcss postcss autoprefixer, then npx tailwindcss init -p) dates from Tailwind v3. Run without a version today, npm installs v4: the init command no longer exists and a v3 postcss.config.js fails. If you hit those errors next, the v4 "Missing specifier" and npx tailwindcss init errors walks through each one. The Tailwind PostCSS plugin has moved explains why the official Vite guide now uses @tailwindcss/vite instead of PostCSS. On v4 the automatic source detection replaces the content list. The dev-versus-build check above still applies: compare the built file with what dev serves before blaming the integration.
When dev is styled and the build is not, compare the CSS files before changing the configuration. These reproductions distinguish asset loading from the default production purge in Tailwind v2 classic; explicit purge activation and JIT need their own source-path checks.
Further reading: Tailwind @apply not working in Storybook: add postcss-loader. Also see Fix Vite: Preprocessor dependency "sass" not found.
Frequently Asked Questions
Still stuck? Paste your error.
Paste the error message or stack trace — we check it against every fix and tool on the site.
Continue Reading
Fix: Tailwind PostCSS Plugin Has Moved (Vite + React)
The exact steps to resolve the Tailwind v4 PostCSS plugin relocation error in a Vite + React + TypeScript build.
Fix slow Tailwind v3 in Vite: remove catch-all safelists
Separate oversized generated CSS from plugin cost, with a single-change compiler reproduction and an Autoprefixer negative control.
Tailwind v4 @source inline(): replace the v3 safelist
Trace an absent grid utility through a production build, replace the legacy safelist, and include shared UI classes without generating every combination.


