Overview
Migrating an existing stylesheet to Tailwind works best component by component, not file by file. Install Tailwind v4, remove global styles that conflict with utility classes, convert one component fully before moving to the next, and extract design tokens to @theme so the custom colors and spacing your codebase already uses map cleanly to Tailwind utilities. The Tailwind reference page is tailwind; the comparison with CSS Modules is tailwind-vs-css-modules.
Prerequisites
- Node 20 or newer.
- A project with an existing CSS file (plain CSS, SCSS, or PostCSS is fine).
- A framework that supports PostCSS: Next.js, Astro, Vite, or similar. See nextjs and astro for framework-specific notes.
Steps
1. Install Tailwind v4
Tailwind v4 uses a single CSS entry point. There is no tailwind.config.js required.
npm install tailwindcss@latest @tailwindcss/vite
# or for PostCSS-based setups
npm install tailwindcss@latest @tailwindcss/postcssVite / Astro
// vite.config.ts
import tailwindcss from "@tailwindcss/vite";
export default { plugins: [tailwindcss()] };PostCSS (Next.js)
// postcss.config.js
module.exports = { plugins: { "@tailwindcss/postcss": {} } };Add the Tailwind import to your main CSS entry point:
@import "tailwindcss";2. Remove conflicting global resets
Your existing stylesheet likely includes resets or normalizations that conflict with Tailwind’s Preflight (which ships built-in with v4). Remove duplicate resets to avoid specificity fights.
Remove any of the following if present:
/* Remove these -- Tailwind's Preflight already handles them */
*, *::before, *::after { box-sizing: border-box; }
body { margin: 0; }
img { max-width: 100%; }Keep custom resets that Tailwind does not cover, such as custom scrollbar styles or font feature settings.
3. Map design tokens to @theme
Tailwind v4 reads design tokens from a @theme block in your CSS. Convert your CSS custom properties to @theme so Tailwind generates matching utilities.
@import "tailwindcss";
@theme {
--color-brand: #1a56db;
--color-brand-dark: #1e429f;
--font-sans: "Inter", ui-sans-serif, system-ui;
--spacing-18: 4.5rem;
}After this, text-brand, bg-brand-dark, font-sans, and m-18 are available as utilities. See css-custom-properties for the broader custom properties pattern this replaces.
4. Convert components incrementally
Pick the smallest, most isolated component first: a button, a badge, a card. Replace its CSS class rules with utility classes in the template, then delete the CSS rules.
Before
<button class="btn-primary">Save</button>.btn-primary {
background: var(--color-brand);
color: white;
padding: 0.5rem 1rem;
border-radius: 0.375rem;
font-weight: 600;
}After
<button class="bg-brand text-white px-4 py-2 rounded-md font-semibold">Save</button>Delete .btn-primary from the CSS. Run the build after each component to confirm nothing broke. Do not convert more than one component per commit; it makes review and rollback tractable.
For patterns used in many places, create a component with @layer components:
@layer components {
.btn-primary {
@apply bg-brand text-white px-4 py-2 rounded-md font-semibold;
}
}This preserves the class name during migration and can be deleted after all call sites use utility classes directly.
5. Lint with the sort plugin
The prettier-plugin-tailwindcss plugin sorts utility classes in a consistent order, which reduces review noise and merge conflicts.
npm install -D prettier prettier-plugin-tailwindcss// .prettierrc
{
"plugins": ["prettier-plugin-tailwindcss"]
}Run Prettier as part of CI to enforce sorted classes on every PR.
Verify it worked
# 1. Build completes without CSS errors.
npm run build
# 2. No orphaned CSS classes remain in the old stylesheet.
grep -r "\.btn-" src/ --include="*.css"
# No output means those rules are gone.
# 3. Tailwind generates the expected utilities.
npx tailwindcss --input src/main.css --output /dev/null --content "src/**/*.{html,ts,tsx}" 2>&1 | grep "Done"Common errors
- Styles missing after migration. A utility class is being purged because the content glob does not cover the template file. Check that
@sourceor the Vite plugin scans all template paths. @themetokens not generating utilities. Token names must follow the--color-*,--spacing-*etc. naming convention. Arbitrary names like--brand-blueare not auto-mapped.- Specificity conflict with a third-party library. The library’s styles override Tailwind utilities. Wrap the library’s import in
@layer baseto lower its specificity, or use!importantutilities (!bg-white) sparingly. - Prettier plugin crashes. The plugin version must match the Tailwind version. Run
npm ls prettier-plugin-tailwindcss tailwindcssand align versions. - Build is much slower after adding Tailwind. In development mode Tailwind watches files. In production mode it scans and generates once. Ensure
NODE_ENV=productionis set in CI.