How to Migrate CSS to Tailwind (Step-by-Step, No Rewrites)
Quick answer: Don't rewrite everything at once. Install Tailwind alongside your existing CSS, convert new components in Tailwind, and migrate old components one at a time starting with layout utilities (flex, grid, spacing). Use @apply as a bridge for complex selectors you aren't ready to inline. A typical 50-page site takes 2-4 weeks of incremental migration without breaking anything.
I migrated a 14,000-line CSS codebase to Tailwind over three weeks. The first mistake I made was trying to convert everything in one weekend. By Sunday night I had broken the navigation, the footer was missing its background, and three form components had lost their hover states. I reverted the whole thing Monday morning.
The second attempt worked. I installed Tailwind alongside the existing CSS, converted one component per day, and deleted old CSS rules only after verifying each replacement. Zero downtime, zero regressions. Here's the exact process.
Before You Start: The Pre-Migration Audit
Don't touch any code until you know what you're working with. Run a quick audit of your current CSS:
1. Count your CSS. Check total lines across all stylesheets. Under 2,000 lines? You can probably migrate in a week. Over 10,000? Plan for 3-4 weeks.
2. Identify dead CSS. Tools like PurgeCSS (standalone) or Chrome DevTools' Coverage tab will show you how much CSS is actually used. On the site I migrated, 38% of the CSS was dead --- rules from deleted pages, old experiments, vendor prefixes for browsers we dropped years ago. Delete dead CSS before you start. No point in migrating rules nobody uses.
3. List your custom values. Go through your stylesheets and note any recurring custom values: specific hex colors, font sizes, spacing values, breakpoints. These become your tailwind.config.js customizations. If your codebase uses #2563EB everywhere, map it to a semantic name like primary in Tailwind's config.
4. Check for CSS-in-JS or preprocessors. If you're using Sass, Less, or styled-components, the migration path is slightly different. Sass nesting and variables map well to Tailwind. CSS-in-JS libraries require converting from JavaScript objects back to class strings, which is more work.
Step 1: Install Tailwind Alongside Existing CSS
This is the critical part --- Tailwind runs in parallel with your old CSS, not instead of it.
npm install -D tailwindcss @tailwindcss/postcss postcss
npx tailwindcss init
Create your Tailwind entry point (tailwind.css or add to your existing entry):
@import "tailwindcss";
Configure tailwind.config.js to scan your template files:
module.exports = {
content: [
'./src/**/*.{html,js,jsx,ts,tsx}',
'./pages/**/*.{html,js,jsx,ts,tsx}',
],
theme: {
extend: {},
},
}
Import your existing CSS after the Tailwind import so your old styles still take precedence where needed. Both systems coexist. Tailwind utilities and your old classes work side by side on the same elements.
At this point, nothing changes visually. Your site looks identical. That's the goal.
Step 2: Map Your CSS Properties to Tailwind
Here's the reference table I kept open during my entire migration. These are the most common CSS properties and their Tailwind equivalents:
| CSS Property | Example Value | Tailwind Class | Notes |
|---|---|---|---|
display: flex | --- | flex | Same for grid, block, inline-flex, etc. |
justify-content: center | --- | justify-center | justify-between, justify-around, justify-end |
align-items: center | --- | items-center | items-start, items-end, items-stretch |
gap: 1rem | 16px | gap-4 | Tailwind uses 4 = 1rem (16px). gap-2 = 0.5rem |
padding: 1rem | 16px | p-4 | px-4 (horizontal), py-4 (vertical), pt-4 (top) |
margin: 0 auto | --- | mx-auto | m-4 (all), mt-4 (top), mb-0 (bottom zero) |
font-size: 1.25rem | 20px | text-xl | text-sm (14px), text-base (16px), text-lg (18px) |
font-weight: 700 | bold | font-bold | font-normal (400), font-semibold (600) |
color: #374151 | gray-700 | text-gray-700 | Uses Tailwind's color palette |
background-color: #fff | white | bg-white | bg-gray-100, bg-blue-500, etc. |
border-radius: 0.5rem | 8px | rounded-lg | rounded (4px), rounded-xl (12px), rounded-full |
width: 100% | --- | w-full | w-1/2 (50%), w-screen, w-auto |
max-width: 1280px | --- | max-w-7xl | max-w-sm (384px), max-w-lg (512px) |
position: relative | --- | relative | absolute, fixed, sticky |
box-shadow | small | shadow-md | shadow-sm, shadow-lg, shadow-xl, shadow-none |
Step 3: Migrate Layout First
Start with layout utilities because they're the easiest to convert and give you the most visible progress.
Flexbox containers. Find every display: flex in your CSS. Each one becomes a flex class plus whatever justify/align/gap utilities you need. A rule like this:
.header-nav {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 1.5rem;
}
Becomes:
<nav class="flex items-center justify-between px-6">
That's 4 CSS properties replaced with 4 Tailwind classes. Delete the .header-nav rule from your stylesheet.
Grid layouts. Same approach. display: grid; grid-template-columns: repeat(3, 1fr); gap: 2rem; becomes grid grid-cols-3 gap-8.
Spacing. This is the biggest category by line count. Every margin and padding rule in your CSS converts to m- and p- utilities. I migrated a 14,000-line codebase and roughly 40% of the lines were some form of spacing declaration. Converting spacing alone eliminated 5,600 lines of CSS.
After layout, your CSS file should be noticeably shorter and your HTML will have Tailwind classes on most structural elements.
Step 4: Migrate Typography and Colors
Font sizes and weights. Map your existing font sizes to Tailwind's scale. If your codebase uses arbitrary pixel values (13px, 15px, 17px), round to the nearest Tailwind size or extend the config:
// tailwind.config.js
module.exports = {
theme: {
extend: {
fontSize: {
'13': '0.8125rem',
'15': '0.9375rem',
'17': '1.0625rem',
}
}
}
}
Colors. This is where tailwind.config.js customization matters most. If your brand uses #2563EB for primary actions, don't use Tailwind's blue-600 (which is #2563EB by coincidence, but might not match your exact shade). Add your colors to the config:
theme: {
extend: {
colors: {
primary: '#2563EB',
secondary: '#7C3AED',
surface: '#F9FAFB',
}
}
}
Now bg-primary, text-secondary, and bg-surface work everywhere and match your design system exactly.
Step 5: Handle Responsive Breakpoints
Vanilla CSS media queries migrate to Tailwind's responsive prefixes. Here's the mapping:
/* Old CSS */
.container {
padding: 1rem;
}
@media (min-width: 768px) {
.container {
padding: 2rem;
}
}
@media (min-width: 1024px) {
.container {
padding: 3rem;
}
}
<!-- Tailwind -->
<div class="p-4 md:p-8 lg:p-12">
Three media query blocks collapse into three classes. This is where Tailwind's density wins --- responsive CSS in vanilla files is verbose because every breakpoint is a separate block with a selector.
Tailwind's default breakpoints are sm (640px), md (768px), lg (1024px), xl (1280px), and 2xl (1536px). If your old CSS uses different breakpoints (say 720px and 960px), either adjust to the nearest Tailwind breakpoint or customize the config.
Step 6: Use @apply for Complex Selectors
Some CSS patterns don't convert cleanly to inline utilities. Pseudo-elements (::before, ::after with complex content), deeply nested selectors, and third-party library overrides are easier to keep as CSS.
The bridge: @apply lets you use Tailwind utilities inside regular CSS rules:
/* Keep this in your CSS file */
.markdown-content h2::after {
content: '';
@apply block w-12 h-0.5 bg-blue-500 mt-2;
}
/* Third-party override */
.react-datepicker__header {
@apply bg-white border-b border-gray-200 p-4;
}
Use @apply as a temporary bridge during migration, not a permanent pattern. If you find yourself writing @apply for everything, you're not actually migrating to Tailwind --- you're just using Tailwind as a verbose Sass replacement. The goal is inline utilities in your HTML. Reserve @apply for cases where inline classes genuinely can't work.
The Gotchas That Will Bite You
Specificity conflicts. Your old CSS classes have specificity. Tailwind utilities also have specificity. When both target the same element, the old class might win because it was loaded later or has a more specific selector. Fix this by adding !important to Tailwind utilities (enabled in config with important: true) or by deleting the old CSS rule before adding the Tailwind replacement.
Hover/focus state duplication. If your old CSS has .button:hover { background: #1d4ed8; } and you add Tailwind's hover:bg-blue-700 to the same element, you get two hover styles competing. Always remove the old CSS rule when adding the Tailwind replacement. Don't leave both.
Custom properties (CSS variables). If your old CSS uses var(--primary-color) extensively, you can reference them in Tailwind using arbitrary values: bg-[var(--primary-color)]. But a cleaner approach is to map those variables to Tailwind's config and drop the CSS variables entirely.
Print styles. Tailwind has a print: variant for print-specific styles, but if your old CSS has a complex @media print block, it's usually easier to keep it as vanilla CSS than to convert 30+ print-specific rules to inline utilities.
Transition and animation properties. Tailwind handles simple transitions (transition, duration-300, ease-in-out) and basic animations (animate-spin, animate-pulse). Complex multi-step @keyframes should stay as vanilla CSS.
Migration Timeline: What to Expect
Based on 3 migrations I've done (ranging from 3,000 to 14,000 lines of CSS):
| Codebase Size | Estimated Time | Daily Pace |
|---|---|---|
| Under 2,000 lines | 3-5 days | 3-5 components/day |
| 2,000-5,000 lines | 1-2 weeks | 2-3 components/day |
| 5,000-15,000 lines | 2-4 weeks | 1-2 components/day |
| Over 15,000 lines | 4-8 weeks | 1 component/day + testing |
For unit conversions during migration --- figuring out what 18px is in rem or what spacing value maps to what Tailwind class --- the CSS unit converter saves time. Paste a pixel value, get the rem equivalent and the closest Tailwind class.
FAQ
Should I migrate everything or just new code?
If the project is actively maintained and growing, migrate incrementally --- new components in Tailwind, old components converted as you touch them. If the project is in maintenance mode with rare changes, don't migrate at all. The cost of migration only pays off if you're actively writing new CSS. Converting 5,000 lines of CSS that nobody touches again is busywork.
Can I use Tailwind with Sass or Less?
Yes, but it's redundant. Tailwind replaces most of what Sass provides: variables become theme config, nesting becomes direct utility classes, mixins become component composition. During migration, Tailwind runs alongside Sass fine --- PostCSS processes both. After migration, you can usually drop Sass entirely.
What about CSS modules in React/Next.js?
CSS modules scope styles to components, which solves the same naming collision problem Tailwind solves with utility classes. You can use both simultaneously during migration. Convert CSS module files one at a time: move the styles from the .module.css file to Tailwind utilities on the JSX elements, then delete the module file.
How do I handle global styles like resets and base typography?
Keep them. Tailwind's @layer base is designed for global styles. Move your CSS reset and base typography rules into a Tailwind base layer or keep them in a separate globals.css file. Don't try to convert * { box-sizing: border-box; } or body { font-family: ... } into utility classes --- that's not what utilities are for.
Next Steps
- Convert CSS rules instantly with the CSS to Tailwind converter --- paste any CSS rule and get the Tailwind utility classes
- Read the full Tailwind vs vanilla CSS comparison if you're still deciding whether to migrate
- Use the CSS unit converter for quick px-to-rem conversions during migration