@theme CSS custom properties, convert mixins to utility classes or @apply rules, replace nesting with CSS-native nesting, then remove the preprocessor once no references remain.npx @tailwindcss/upgrade (automates ~90% of Tailwind v3-to-v4 migration including class renames, config-to-CSS conversion, and template updates; Sass files require manual conversion)@import "tailwindcss" inside .scss or .less files. The Oxide engine (Lightning CSS) cannot parse preprocessor syntax. Preprocessor code must be converted to plain CSS first. [src4]@tailwindcss/upgrade tool only processes .css files. It will skip or error on .scss/.less files. You must manually convert preprocessor code to plain CSS before or after running the tool. [src1, src5]@import "tailwindcss" appear in a .scss file — Lightning CSS will crash with parse errors on Sass syntax. Keep Tailwind directives in .css files only. [src6]color-mix() (the replacement for Sass darken()/lighten()) requires Safari 16.4+, Chrome 111+, Firefox 113+. For wider browser support, pre-define all color shades in @theme. [src3]| Sass/LESS Pattern | Tailwind Equivalent | Example |
|---|---|---|
$primary: #3B82F6; / @primary: #3B82F6; | @theme { --color-primary: #3B82F6; } | class="text-primary bg-primary/10" |
@mixin button($bg) { ... } | Utility classes or @apply | class="px-4 py-2 rounded bg-blue-500" |
@include respond-to('md') { ... } | Responsive prefix | class="p-4 md:p-8 lg:p-12" |
darken($color, 10%) / lighten() | Opacity modifier or color-mix() | class="bg-blue-500/90" or color-mix(in oklch, ...) |
&:hover { color: red; } | State variant prefix | class="hover:text-red-500" |
@extend .btn-base; | Component extraction with @apply | .btn { @apply px-4 py-2 rounded; } |
.card { .title { ... } } nesting | CSS-native nesting or flat utilities | .card { & .title { @apply text-lg font-bold; } } |
@for $i from 1 through 12 { ... } | Grid/spacing scale utilities | class="grid-cols-1 md:grid-cols-6 lg:grid-cols-12" |
@import 'variables'; @import 'mixins'; | @import "tailwindcss"; | Single import replaces all partials |
map-get($colors, 'primary') | CSS variable reference | var(--color-primary) or theme(colors.primary) |
@each $name, $color in $colors { ... } | @theme block generates all utilities | @theme { --color-brand: #38bdf8; } auto-generates utilities |
$spacing-unit: 8px; padding: $spacing-unit * 2; | Spacing scale | class="p-4" (= 1rem = 16px) |
@media (min-width: $breakpoint-md) { ... } | Responsive prefix | class="md:flex md:gap-4" |
%placeholder { ... } silent extends | @utility directive (v4) | @utility glass { backdrop-filter: blur(12px); } |
!default variable flags | @theme with CSS custom property fallbacks | --color-accent: var(--color-brand, #3B82F6); |
START
├── Is the project currently on Tailwind v3 with Sass alongside?
│ ├── YES → First upgrade to Tailwind v4 (run npx @tailwindcss/upgrade), then remove Sass
│ └── NO ↓
├── Is the project pure Sass/LESS with no Tailwind at all?
│ ├── YES → Install Tailwind v4, run both systems in parallel, migrate file-by-file
│ └── NO ↓
├── Does the Sass/LESS codebase use complex mixins with parameters?
│ ├── YES → Extract to component classes with @apply, or convert to CSS custom properties + calc()
│ └── NO ↓
├── Does the project use Sass maps or LESS maps for design tokens?
│ ├── YES → Convert to @theme block with CSS custom properties (auto-generates utility classes)
│ └── NO ↓
├── Are there many @extend / placeholder selectors?
│ ├── YES → Replace with @apply or direct utility classes in markup
│ └── NO ↓
├── Does the project use Sass color functions (darken, lighten, mix)?
│ ├── YES → Replace with Tailwind opacity modifiers, color-mix(), or pre-defined shades in @theme
│ └── NO ↓
└── DEFAULT → Replace Sass/LESS features one-by-one: variables → @theme, nesting → CSS nesting,
imports → @import "tailwindcss", then delete .scss/.less files
Quantify what needs migrating: count variables, mixins, extends, nested rules, and custom functions. This tells you migration complexity and timeline. [src5]
# For Sass/SCSS projects
find . -name '*.scss' -o -name '*.sass' | xargs wc -l | tail -1
grep -rn '\$' --include='*.scss' | grep -v node_modules | wc -l # variables
grep -rn '@mixin' --include='*.scss' | wc -l # mixins
grep -rn '@include' --include='*.scss' | wc -l # mixin usages
grep -rn '@extend' --include='*.scss' | wc -l # extends
Verify: Small project: <500 lines, <20 variables. Medium: 500–5000 lines. Large: >5000 lines. Estimate 1–2 hours per 500 lines.
Run both systems in parallel during migration. Tailwind processes .css files; Sass processes .scss files. They do not conflict if kept in separate files. [src1]
# Install Tailwind v4 (requires Node.js 20+)
npm install tailwindcss @tailwindcss/postcss
# For Vite projects (recommended — fastest builds)
npm install @tailwindcss/vite
# For CLI-only usage
npm install @tailwindcss/cli
Create your Tailwind entry point as a .css file (NOT .scss):
/* src/tailwind.css */
@import "tailwindcss";
Verify: npx @tailwindcss/cli -i src/tailwind.css -o dist/output.css compiles without errors.
Move design tokens from $variables or @variables to Tailwind's @theme block. Each CSS custom property automatically generates utility classes. Variables defined outside @theme (e.g., in :root) do NOT generate utilities. [src3, src7]
/* BEFORE: _variables.scss */
/* $primary: #3B82F6; $secondary: #10B981; $font-sans: 'Inter', sans-serif; */
/* AFTER: tailwind.css */
@import "tailwindcss";
@theme {
--color-primary: #3B82F6;
--color-secondary: #10B981;
--font-sans: 'Inter', sans-serif;
}
Verify: class="text-primary bg-secondary" renders correct colors in the browser.
Sass mixins that output simple property groups become utility classes in HTML. Complex mixins with parameters become custom CSS with @apply or CSS custom properties. In v4, use the @utility directive instead of @layer utilities. [src2, src6]
// BEFORE: Sass mixin
@mixin card-style($padding: 16px, $radius: 8px) {
padding: $padding;
border-radius: $radius;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
background: white;
}
<!-- AFTER: Tailwind utility classes -->
<div class="p-4 rounded-lg shadow-sm bg-white">...</div>
For truly reusable patterns, use @utility in CSS (v4 replacement for @layer utilities):
@utility card {
@apply p-4 rounded-lg shadow-sm bg-white;
}
Verify: Visual diff shows identical rendering before and after mixin removal.
Replace Sass nesting with CSS-native nesting (supported in v4 via Lightning CSS) or flat utility classes. Replace @media queries with responsive prefixes. [src1]
// BEFORE: Sass nesting + media queries
.sidebar {
width: 100%;
padding: 1rem;
@media (min-width: 768px) { width: 250px; padding: 2rem; }
.nav-item { color: gray; &:hover { color: blue; } }
}
<!-- AFTER: Tailwind utility classes -->
<aside class="w-full p-4 md:w-[250px] md:p-8">
<a class="text-gray-500 hover:text-blue-500">...</a>
</aside>
Verify: Resize browser window — responsive behavior matches original breakpoints.
Replace Sass partials with Tailwind's single @import "tailwindcss". Replace @extend with direct utility classes or @apply. Tailwind v4 automatically bundles imported CSS files without separate preprocessing. [src1, src2, src4]
/* AFTER: Tailwind CSS with @utility */
@import "tailwindcss";
@utility btn-primary {
@apply inline-flex items-center px-4 py-2 rounded-md bg-primary text-white;
}
@utility btn-secondary {
@apply inline-flex items-center px-4 py-2 rounded-md bg-secondary text-white;
}
Verify: grep -rn '@import\|@extend' --include='*.scss' returns zero results after migration.
In Tailwind v4, @apply inside component <style> blocks requires a @reference directive pointing to your main CSS file. Alternatively, use utility classes directly in markup (recommended). [src1, src4]
<!-- Vue/Svelte: If you must use @apply in scoped styles -->
<style scoped>
@reference "../../app.css";
.card-title { @apply text-xl font-bold text-gray-900; }
</style>
Verify: Component renders correctly with @apply styles applied.
Once all .scss / .less files are converted, remove the preprocessor toolchain. [src5]
# Remove Sass
npm uninstall sass sass-loader node-sass
# Remove LESS
npm uninstall less less-loader
# Delete all .scss / .less files
find . -name '*.scss' -o -name '*.less' | grep -v node_modules | xargs rm
Verify: npm ls sass less shows no preprocessor packages. App builds and runs correctly.
/* Input: A Sass-based design system with variables, mixins, and custom breakpoints
Output: Equivalent Tailwind v4 CSS-first configuration */
@import "tailwindcss";
/* Design tokens — replaces $variables in _variables.scss */
@theme {
--color-brand-50: oklch(0.97 0.01 250);
--color-brand-500: oklch(0.55 0.2 250);
--color-brand-900: oklch(0.25 0.1 250);
--color-surface: #ffffff;
--color-surface-dark: #1a1a2e;
--font-sans: 'Inter', ui-sans-serif, system-ui, sans-serif;
--font-mono: 'JetBrains Mono', ui-monospace, monospace;
--breakpoint-xs: 30rem; /* 480px */
--breakpoint-3xl: 120rem; /* 1920px */
--radius-pill: 9999px;
--shadow-card: 0 1px 3px oklch(0 0 0 / 0.08), 0 1px 2px oklch(0 0 0 / 0.06);
}
/* Component classes — replaces @mixin card-style, @mixin button-base */
@utility card {
@apply rounded-lg bg-surface p-6 shadow-card;
}
@utility btn {
@apply inline-flex items-center justify-center gap-2 rounded-md px-4 py-2
font-medium transition-colors duration-150
focus:outline-2 focus:outline-offset-2 focus:outline-brand-500;
}
@utility btn-primary {
@apply btn bg-brand-500 text-white hover:bg-brand-900;
}
/* Dark mode — replaces Sass if/else theme switching */
@variant dark (&:where(.dark, .dark *));
// Input: React component using CSS Modules with Sass (.module.scss)
// Output: Same component using Tailwind utility classes
// BEFORE: Button.module.scss + import styles from './Button.module.scss'
// .button { @include button-base; &--primary { background: $primary; } }
// AFTER: Button.tsx with Tailwind (no separate stylesheet)
import { type ButtonHTMLAttributes } from 'react';
const variants = {
primary: 'bg-brand-500 text-white hover:bg-brand-900 focus:ring-brand-500',
secondary: 'bg-secondary text-white hover:bg-secondary/80 focus:ring-secondary',
ghost: 'bg-transparent text-brand-500 hover:bg-brand-50 focus:ring-brand-500',
} as const;
type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
variant?: keyof typeof variants;
};
export function Button({ variant = 'primary', className = '', disabled, children, ...props }: ButtonProps) {
return (
<button
className={`inline-flex items-center justify-center gap-2 rounded-md px-4 py-2
font-medium transition-colors duration-150
focus:outline-2 focus:outline-offset-2
${variants[variant]}
${disabled ? 'opacity-50 cursor-not-allowed' : ''}
${className}`}
disabled={disabled}
{...props}
>
{children}
</button>
);
}
<!-- Input: Vue component using <style lang="scss" scoped>
Output: Same component using Tailwind utilities + @reference -->
<!-- BEFORE: Vue with scoped Sass -->
<!-- <style lang="scss" scoped>
@import '@/styles/variables';
.alert { padding: $spacing-md; border-radius: $radius-md; border: 1px solid;
&--success { border-color: $success; background: lighten($success, 40%); }
&--error { border-color: $danger; background: lighten($danger, 40%); } }
</style> -->
<!-- AFTER: Vue with Tailwind v4 -->
<template>
<div :class="[
'rounded-md border p-4',
variant === 'success' && 'border-green-500 bg-green-50',
variant === 'error' && 'border-red-500 bg-red-50',
]" role="alert">
<h3 class="mb-1 font-semibold">{{ title }}</h3>
<p class="text-sm"><slot /></p>
</div>
</template>
<script setup lang="ts">
defineProps<{ variant: 'success' | 'error'; title: string }>();
</script>
<!-- If you still need custom CSS, use @reference for @apply -->
<style scoped>
@reference "../../app.css";
h3 { @apply text-lg font-bold; }
</style>
/* BAD — Sass syntax in a file processed by Tailwind v4 */
@import "tailwindcss";
$primary: #3B82F6; /* Sass variable — will not compile */
.btn {
background: $primary;
@include responsive-padding; /* Sass mixin — will fail */
}
/* GOOD — Pure CSS with Tailwind v4 @theme */
@import "tailwindcss";
@theme {
--color-primary: #3B82F6;
}
.btn {
background: var(--color-primary);
@apply px-4 py-2 md:px-6 md:py-3;
}
/* BAD — Re-creating Sass abstractions with @apply defeats the purpose */
.container { @apply mx-auto max-w-7xl px-4; }
.heading-1 { @apply text-4xl font-bold text-gray-900; }
.heading-2 { @apply text-3xl font-semibold text-gray-800; }
.heading-3 { @apply text-2xl font-medium text-gray-700; }
.paragraph { @apply text-base text-gray-600 leading-relaxed; }
.link { @apply text-blue-500 underline hover:text-blue-700; }
/* 200 more classes... You just rebuilt Sass with extra steps */
<!-- GOOD — Utility classes in HTML; extract only truly reusable components -->
<h1 class="text-4xl font-bold text-gray-900">Title</h1>
<p class="text-base text-gray-600 leading-relaxed">Content</p>
<!-- Only extract with @utility when repeated in 3+ places -->
# BAD — The upgrade tool does not recognize .scss files
npx @tailwindcss/upgrade
# "Cannot find any CSS files that reference Tailwind CSS"
# GOOD — Extract Tailwind directives to a temp .css file first
echo '@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";' > tailwind.tmp.css
npx @tailwindcss/upgrade --force
git diff tailwind.tmp.css # Review, apply to real styles
rm tailwind.tmp.css
// BAD — Running Sass just for nesting when CSS nesting is native
// sass-loader + Tailwind = two build steps, potential conflicts
.card {
.title { font-size: 1.25rem; font-weight: bold; }
.body { padding: 1rem; }
}
/* GOOD — CSS nesting works natively in Tailwind v4 */
.card {
& .title { @apply text-xl font-bold; }
& .body { @apply p-4; }
}
/* BAD — Variables in :root do not generate Tailwind utilities */
:root {
--color-brand: #3B82F6;
--color-accent: #10B981;
}
/* class="text-brand" will NOT work */
/* GOOD — @theme variables auto-generate utilities */
@theme {
--color-brand: #3B82F6;
--color-accent: #10B981;
}
/* class="text-brand bg-accent/50" now works */
.scss/.less files: Tailwind's Oxide engine (built on Lightning CSS) cannot parse Sass or LESS syntax. Fix: Keep Tailwind directives in .css files only. Run preprocessor and Tailwind as separate build steps during migration. [src1, src6]theme() function unavailable in Sass: Since Sass compiles before PostCSS/Tailwind, Tailwind's theme() function cannot be used inside .scss files. Fix: Use CSS custom properties (var(--color-primary)) instead. [src2]@apply with !important fails in Sass: Sass misinterprets the !important flag. Fix: Use interpolation syntax: @apply bg-red-500 #{!important};. Better yet, migrate to plain .css. [src2]@apply rule negates Tailwind's benefits. Fix: Apply utilities directly in HTML markup. Reserve @apply/@utility for patterns repeated in 3+ places. [src1]darken(), lighten(), mix() are compile-time Sass functions. Fix: Use Tailwind opacity modifiers (bg-blue-500/80), CSS color-mix(), or define color shades in @theme. [src3]@reference in Vue/Svelte scoped styles: In Tailwind v4, @apply in component <style> blocks requires a @reference directive. Fix: Add @reference "../../app.css"; at the top of the <style> block. [src1, src4]@theme do not generate Tailwind utilities. Fix: All design tokens that need utility classes must be inside @theme { }, not in plain :root { }. [src3, src7]@for/@each loops for grid classes: Tailwind's JIT engine already generates these on-demand. Fix: Delete Sass loops and use Tailwind's built-in grid-cols-*, gap-*, and arbitrary value syntax. [src7]@variable) look identical to CSS at-rules (@media, @import, @theme). Fix: Convert LESS variables to CSS custom properties (--variable) early in the migration. [src4]# Count remaining Sass/LESS files
find . -name '*.scss' -o -name '*.sass' -o -name '*.less' | grep -v node_modules | wc -l
# Count Sass variable references still in use
grep -rn '\$[a-zA-Z]' --include='*.scss' --include='*.vue' --include='*.svelte' | grep -v node_modules | wc -l
# Count LESS variable references
grep -rn '@[a-zA-Z]' --include='*.less' | grep -v node_modules | grep -v '@media\|@import\|@charset\|@keyframes\|@font-face' | wc -l
# Check for Sass dependencies in package.json
node -e "const p=require('./package.json'); const deps={...p.dependencies,...p.devDependencies}; const sass=Object.keys(deps).filter(k=>k.match(/sass|less|stylus/i)); console.log(sass.length?'Still installed: '+sass.join(', '):'Clean');"
# Verify Tailwind v4 is working
npx @tailwindcss/cli -i src/app.css -o /dev/null 2>&1 && echo "OK" || echo "Failed"
# Check generated CSS size
npx @tailwindcss/cli -i src/app.css -o dist/output.css && wc -c dist/output.css
# Find remaining @import of Sass partials
grep -rn "@import '.*'" --include='*.scss' --include='*.css' | grep -v node_modules | grep -v tailwindcss
# Verify Node.js version meets minimum requirement
node -v # Must be v20.0.0 or higher for Tailwind v4
| Version | Status | Breaking Changes | Migration Notes |
|---|---|---|---|
| Tailwind v4.1 (Apr 2025) | Current | Text shadows, masks, @source not/inline(), improved browser fallbacks | Minor patch on v4.0 — no Sass-specific changes |
| Tailwind v4.0 (Jan 2025) | Current | Oxide engine (Lightning CSS), @import "tailwindcss" replaces @tailwind, CSS-first @theme config, no Sass/LESS support | postcss-import and autoprefixer no longer needed; Sass files must be converted to CSS |
| Tailwind v3.4 (Dec 2023) | LTS | Last version supporting Sass/LESS coexistence | Stay here if you cannot remove Sass yet |
| Tailwind v3.0 (Dec 2021) | Maintenance | JIT engine default | Content paths required in tailwind.config.js |
| Sass 1.x (Dart Sass) | Current | @import deprecated for @use/@forward | Convert Sass @import to @use before migrating to Tailwind |
| LESS 4.x | Current | — | LESS @var conflicts with CSS at-rules; rename before converting |
| Use When | Don't Use When | Use Instead |
|---|---|---|
| Starting a new project or major redesign | Sass codebase is stable and team is productive | Keep Sass, add Tailwind incrementally |
| Design system is defined in design tokens | Heavy use of Sass programmatic features (functions, control flow) | Keep Sass for complex theming, use Tailwind for layout |
| Team wants utility-first workflow and faster prototyping | Project has <500 lines of CSS and no complexity | Vanilla CSS or CSS Modules |
| Bundle size is a concern (Tailwind purges unused CSS) | Need to support IE11 or Safari <16.4 | Stay on Tailwind v3 + Sass, or PostCSS alone |
| Want to eliminate Sass build step overhead | Library/design system consumed by multiple apps with varying build setups | CSS custom properties + vanilla CSS |
@tailwindcss/upgrade tool does not process .scss or .less files. You must manually convert preprocessor code to plain CSS before or after running the tool.@apply is still supported in v4 but is considered an escape hatch. Tailwind Labs recommends using utility classes directly in markup, and @utility for truly reusable component patterns.@variable) collides with CSS at-rules (@media, @import, @theme). During migration, this can cause confusing parser errors. Convert LESS variables to CSS custom properties early.color-mix() (the replacement for Sass darken()/lighten()) requires Safari 16.4+, Chrome 111+, Firefox 113+. For wider support, pre-define color shades in @theme.@source not can exclude legacy Sass directories from Tailwind's class detection scanning, speeding up builds during incremental migration. [src8]