Vite not working with Tailwind: fix it, or use one HTML file

Vite is a build tool, and Tailwind classes that do nothing inside it have three usual causes. If all you need is one page that works, a single HTML file needs none of them.

Vite (pronounced "veet") is a build tool for web projects, not a library you add to a page. When Tailwind styles do nothing in it, one of three things is usually wrong.

The CSS is never loaded, the setup belongs to the other Tailwind version, or a class name is built from pieces. Below are the fixes. If you only need one page that works, skip the build entirely:

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>One file, Tailwind and a module script</title>
<!-- Tailwind in the browser: no npm, no config, no build -->
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4.3.3"></script>
</head>
<body class="min-h-screen bg-slate-100 font-sans text-slate-800 grid place-items-center p-4">
  <main class="w-full max-w-xs rounded-2xl bg-white p-6 shadow-lg text-center">
    <h1 class="text-xl font-bold">Hello from one file</h1>
    <p class="mt-1 text-sm text-slate-500">Tailwind classes and a module script, no build.</p>
    <p id="count" class="mt-4 text-5xl font-extrabold text-indigo-600">0</p>
    <button id="add" class="mt-4 rounded-lg bg-indigo-600 px-5 py-2 font-semibold text-white hover:bg-indigo-500 active:scale-95">
      Add one
    </button>
  </main>

  <script type="module">
    // An inline module script runs like any other script; modules are deferred automatically
    const count = document.getElementById('count');
    let n = 0;
    document.getElementById('add').addEventListener('click', () => {
      n += 1;
      count.textContent = n;
    });
  </script>
</body>
</html>
One file: a Tailwind script tag and an inline module script. No npm, no config. Click the button.

That page is one file you can open, edit and send. Everything below explains why the Vite route is different and what to check when it breaks.

What Vite does, and why it is not one file

Vite's own guide describes a dev server built on native ES modules, plus a build command that bundles your code into static assets for production. Its index.html is source code, and the scripts and stylesheets it references are processed as part of the app.

A Vite project builds into a dist folder of several files, while a single HTML file stays one file.
A Vite project builds into a dist folder of several files, while a single HTML file stays one file.

So a Vite project ends in a folder, not a page. The folder is fine for a host. It is awkward when you wanted a file to open or send.

Why Tailwind does nothing in Vite

Three causes cover almost every "my classes are ignored" report.

Three causes of unstyled Tailwind in Vite, each with its fix.
Three causes of unstyled Tailwind in Vite, each with its fix.
  1. The CSS is never loaded. A stylesheet that contains the Tailwind import does nothing until something imports it from a script or links it from index.html. Vite's docs say importing a .css file injects it with a <style> tag.
  2. The setup belongs to the other version. Version 3 used @tailwind directives and a content list. Version 4 uses a plain CSS import and a Vite plugin.
  3. A class name is built from pieces. Tailwind reads your files as plain text, so 'bg-' + color never contains the word it needs.

The Vite fix for Tailwind 4

For Tailwind 4, install two packages, then add the plugin to the Vite config:

// vite.config.js
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'

export default defineConfig({
  plugins: [tailwindcss()],
})

Then put one line in your CSS file, and make sure the file is loaded:

@import "tailwindcss";

The install command is npm install tailwindcss @tailwindcss/vite. If your project still has @tailwind base lines, those belong to version 3. A tailwind.config.js file is no longer detected automatically in version 4. Load it with @config if you still need it.

On version 3, the setup is different: PostCSS, and a content array in tailwind.config.js that lists index.html and your source files. If a class works in one file but not another, check that list first.

Check Tailwind at runtime in a plain file

In a plain HTML file there is no build to blame, so you can ask the browser directly. This page measures whether a Tailwind class produced real CSS, and shows the safe way to switch classes.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Is Tailwind running? Complete class names</title>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4.3.3"></script>
</head>
<body class="min-h-screen bg-slate-100 font-sans text-slate-800 p-4">
  <main class="mx-auto max-w-sm space-y-4">

    <section class="rounded-xl bg-white p-4 shadow">
      <h2 class="font-bold">1. Is Tailwind running?</h2>
      <!-- probe: p-4 means 1rem of padding when Tailwind has generated it -->
      <div id="probe" class="p-4 absolute invisible">probe</div>
      <p id="status" class="mt-2 rounded-md bg-slate-200 px-3 py-2 text-sm">Checking...</p>
    </section>

    <section class="rounded-xl bg-white p-4 shadow">
      <h2 class="font-bold">2. Complete class names</h2>
      <p class="mt-1 text-sm text-slate-500">Each button class is written out in full, so a build tool can find it.</p>
      <div id="badge" class="mt-3 rounded-md px-3 py-2 text-sm font-semibold">Pick a state</div>
      <div class="mt-3 flex gap-2">
        <button data-state="ok" class="rounded-lg bg-green-600 px-3 py-1.5 text-sm font-semibold text-white">OK</button>
        <button data-state="warn" class="rounded-lg bg-amber-500 px-3 py-1.5 text-sm font-semibold text-white">Warn</button>
        <button data-state="fail" class="rounded-lg bg-red-600 px-3 py-1.5 text-sm font-semibold text-white">Fail</button>
      </div>
      <p id="applied" class="mt-3 break-words font-mono text-xs text-slate-500"></p>
    </section>

  </main>

  <script type="module">
    // 1. Ask the browser what the probe's padding really is
    const probe = document.getElementById('probe');
    const status = document.getElementById('status');
    function check() {
      const pad = getComputedStyle(probe).paddingTop;
      status.textContent = pad === '16px'
        ? 'Yes: p-4 gave ' + pad + ' of padding.'
        : 'No: p-4 gave ' + pad + '. The script did not load or run.';
    }
    window.addEventListener('load', () => setTimeout(check, 300));

    // 2. Whole class strings in a lookup, never 'bg-' + color + '-100'
    const states = {
      ok:   'bg-green-100 text-green-800',
      warn: 'bg-amber-100 text-amber-800',
      fail: 'bg-red-100 text-red-800',
    };
    const badge = document.getElementById('badge');
    const applied = document.getElementById('applied');
    document.querySelectorAll('[data-state]').forEach((btn) => {
      btn.addEventListener('click', () => {
        const cls = states[btn.dataset.state];
        badge.className = 'mt-3 rounded-md px-3 py-2 text-sm font-semibold ' + cls;
        badge.textContent = 'State: ' + btn.dataset.state;
        applied.textContent = cls;
      });
    });
  </script>
</body>
</html>
Part 1 reads the padding Tailwind generated. Part 2 swaps whole class strings from a lookup.

The lookup object is the pattern Tailwind's own docs recommend: complete class names in your code, never fragments joined together. It works the same inside a Vite project and in a single file.

Opening dist/index.html from disk

A common surprise comes after the build. You double-click dist/index.html and the page does not run.

A built page opened from a file address versus one served from an address.
A built page opened from a file address versus one served from an address.

Two things are involved. MDN says module-based pages loaded from file:// hit CORS errors and need a server. And Vite's default base is /, so asset paths start at the site root. Setting base to ./ makes the paths relative, which suits embedded deployment.

To check a build locally, use vite preview, which serves the dist folder. Vite's docs say it is not meant as a production server.

Hosting a built front end covers the base path and routing in more depth, and relative and absolute paths explains why the leading slash matters.

If you want a single output file from Vite itself, the third-party plugin vite-plugin-singlefile inlines JavaScript and CSS into dist/index.html. It is MIT licensed, and its README lists its limits.

A finished one-file page

Here is a small tool built the single-file way: Tailwind from one script tag, an inline module script, and a form handled inside the page. Nothing is sent anywhere.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Split the bill</title>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4.3.3"></script>
</head>
<body class="min-h-screen bg-slate-100 font-sans text-slate-800 p-4">
  <main class="mx-auto max-w-sm rounded-2xl bg-white p-5 shadow-lg">
    <h1 class="text-xl font-bold">Split the bill</h1>
    <p class="text-sm text-slate-500">One HTML file. Change a number and the result updates.</p>

    <form id="form" class="mt-4 space-y-3">
      <label class="block text-sm font-medium">Bill
        <input id="bill" type="number" min="0" step="0.01" value="80.00" inputmode="decimal"
          class="mt-1 w-full rounded-lg border border-slate-300 px-3 py-2 text-base">
      </label>
      <label class="block text-sm font-medium">Tip: <span id="tipLabel">15</span>%
        <input id="tip" type="range" min="0" max="30" value="15" class="mt-1 w-full">
      </label>
      <label class="block text-sm font-medium">People
        <input id="people" type="number" min="1" max="20" value="3" inputmode="numeric"
          class="mt-1 w-full rounded-lg border border-slate-300 px-3 py-2 text-base">
      </label>
    </form>

    <div class="mt-4 grid grid-cols-2 gap-3 text-center">
      <div class="rounded-xl bg-indigo-50 p-3">
        <div class="text-xs uppercase tracking-wide text-indigo-700">Each pays</div>
        <div id="each" class="text-2xl font-extrabold text-indigo-700">0.00</div>
      </div>
      <div class="rounded-xl bg-slate-100 p-3">
        <div class="text-xs uppercase tracking-wide text-slate-600">Total</div>
        <div id="total" class="text-2xl font-extrabold">0.00</div>
      </div>
    </div>
  </main>

  <script type="module">
    const $ = (id) => document.getElementById(id);

    function update() {
      const bill = parseFloat($('bill').value) || 0;
      const tip = Number($('tip').value);
      const people = Math.max(1, parseInt($('people').value, 10) || 1);
      const total = bill * (1 + tip / 100);
      $('tipLabel').textContent = tip;
      $('total').textContent = total.toFixed(2);
      $('each').textContent = (total / people).toFixed(2);
    }

    // The form is handled in the page; nothing is sent anywhere
    $('form').addEventListener('input', update);
    $('form').addEventListener('submit', (e) => e.preventDefault());
    update();
  </script>
</body>
</html>
A bill splitter in one HTML file. Change the bill, the tip or the number of people.
  • Tailwind: one script tag in the head, pinned to a version so the page does not change under you.
  • Logic: one type="module" script. Modules are deferred automatically, so it runs after the page is parsed.
  • Form: the submit event calls preventDefault(), and the input event updates the numbers.

For adding Tailwind to a page you already have, see how to add Tailwind to an HTML file. The Tailwind HTML template is a full page to paste.

When it does not work

Symptom Cause Fix
No Tailwind styling in a Vite page The CSS file is never loaded Import it from main.js, or link it in index.html
Classes ignored on version 3 The content list misses your files List index.html and your src files
Setup lines from a guide fail Version 3 lines in a version 4 project Use @import "tailwindcss"; and the Vite plugin
tailwind.config.js has no effect Not detected automatically in version 4 Load it with @config
A colour class works only sometimes The class name is built from pieces Write each class out in full
dist/index.html does not run from disk Module scripts need a server Use vite preview, or one HTML file
Single file is unstyled The CDN script did not load Check the script address and the connection

A built dist folder is hard to send, and a single HTML file attached to a message may open as plain code, or not at all, on a phone. Opening an HTML file on a phone covers why.

To send the working version, paste the page into a NOS document and choose Create share link. HTML to link walks through it.

The page renders as written and its scripts run, so the people you send it to can click the buttons themselves. Scripts from cdn.jsdelivr.net load. If you change the code later, the same link shows the new version.

Questions people ask

Why is Tailwind not working in my Vite project?

Usually the CSS file with the Tailwind import is never loaded, the setup lines belong to the other Tailwind version, or a class name is built from pieces so Tailwind never sees it written out. Check those three in that order.

Do I need Vite to use Tailwind?

No. Tailwind offers a script tag, the Play CDN, that works in a plain HTML file. Its documentation says it is designed for development purposes only and is not intended for production, so a public site is better served by a compiled stylesheet.

Can I open the Vite dist folder by double-clicking index.html?

Often not. MDN notes that module-based pages loaded from a file:// address run into CORS errors and need a server. Vite also provides vite preview, which serves the dist folder locally so you can check the build.

Can Vite produce a single HTML file?

A separate plugin, vite-plugin-singlefile, inlines JavaScript and CSS into dist/index.html. Its README lists limits, such as files in the public folder not being inlined and a single entry point only.

Will a Tailwind script tag still work after I share the page?

In a NOS share link it does. Scripts and styles from cdn.jsdelivr.net load there, so a page that pulls Tailwind from that host looks the same to the person who opens the link.

Keep reading