3 min read

Getting Started with Tailwind CSS v4 in a New Project

  • #tutorial
  • #setup
  • #tooling

Tailwind v4 changed enough that older tutorials will actively mislead you. The biggest shift: no tailwind.config.js by default anymore. Configuration happens in CSS.

Installing

Most build tools now get a dedicated Vite plugin instead of a PostCSS-only setup, which cuts a build step out of the pipeline entirely:

npm install tailwindcss @tailwindcss/vite

Register it in your Vite config:

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

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

Framework-specific setups (Astro, Next.js, SvelteKit) each have their own equivalent. For Astro, the plugin goes under vite.plugins in astro.config.mjs, not the integrations array. It’s a Vite plugin, not an Astro integration, and it’s an easy place to get tripped up following a guide written for a different framework.

Config lives in CSS now

Instead of a JS file, you bring Tailwind in with one import in your main CSS file:

/* global.css */
@import "tailwindcss";

That replaces the old @tailwind base; @tailwind components; @tailwind utilities; trio. Your CSS file is now the source of truth.

Theming

Colors, fonts, and spacing get defined in an @theme block instead of theme.extend:

@theme {
  --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
  --color-brand: oklch(0.6 0.2 260);
}

Any custom property in @theme automatically becomes a usable utility. --color-brand gives you bg-brand, text-brand, border-brand, for free. This is a genuine improvement over v3, where extending the theme and using the new value lived in two separate files you had to keep in sync yourself.

The same pattern covers spacing scales, breakpoints, animation timing. One mental model instead of a different override mechanism per category.

Dark mode and arbitrary values

Dark mode still works the same way at the class level, dark:bg-black alongside bg-white, but v4 lets you redefine what triggers it directly in CSS instead of a darkMode key in a config file:

@custom-variant dark (&:where(.dark, .dark *));

That example switches dark mode from the OS-level prefers-color-scheme media query to a .dark class you toggle yourself, useful if you want a manual light/dark toggle instead of following the system setting. Arbitrary values (w-[137px], bg-[#1da1f2]) work exactly as they did in v3, no change needed there.

Plugins

Registered in CSS with @plugin:

@import "tailwindcss";
@plugin "@tailwindcss/typography";

You still install the plugin as an npm package first. @plugin just tells Tailwind to load it. If a class from a plugin isn’t showing up, check the package is actually installed before assuming the @plugin line is wrong. That’s the more common mistake, and I’ve lost twenty minutes to it more than once.

Editor setup

The official Tailwind CSS IntelliSense extension for VS Code needs no special v4 configuration, it picks up the @theme block automatically and gives you autocomplete for custom colors and spacing the same way it did for theme.extend values in v3. If autocomplete for a custom utility isn’t showing up, the usual culprit is the extension pointed at the wrong CSS file in a monorepo with multiple global.css files, not a v4-specific bug.

What changed under the hood

Beyond config syntax, v4’s engine is a from-scratch Rust-backed build, often several times faster on cold and incremental builds, and it auto-detects your content sources. No content: [] glob array. It scans based on where the CSS import lives and standard ignore rules, which kills an entire category of “why isn’t my class generating” bugs caused by a glob nobody remembered to update.

Migrating an existing v3 project

Run npx @tailwindcss/upgrade before attempting anything manual. It handles the mechanical parts, renaming, restructuring, import syntax, and leaves you to sanity-check the result instead of hand-translating every theme key. It won’t catch everything, especially heavily customized configs with custom plugins, so budget time to manually check anything unusual afterward. Run your test suite and eyeball a few pages too: a class that silently stopped generating won’t throw an error. It’ll just quietly look wrong, and you might not notice for a week.