
Tailwind Refactor
- 254 installs
- 191 repo stars
- Updated July 24, 2026
- pproenca/dot-skills
tailwind-refactor: A skill for development. This provides functionality for development workflows.
Key points
- tailwind-refactor
Tailwind Refactor by the numbers
- 254 all-time installs (skills.sh)
- +7 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #1,498 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/pproenca/dot-skills --skill tailwind-refactorAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 254 |
|---|---|
| repo stars | ★ 191 |
| Last updated | July 24, 2026 |
| Repository | pproenca/dot-skills ↗ |
How do I use tailwind-refactor for development tasks?
Use tailwind-refactor for development tasks
Who is it for?
Best when you're working on backend & apis and need structured help with tailwind-refactor.
Skip if: Teams with no backend & apis needs, or anyone wanting a generic chat assistant without this specific workflow.
When should I use this skill?
When you need to use tailwind-refactor for development tasks, or when tailwind-refactor: a skill for development. this provides functionality for development workflows.
What you get
Structured output aligned to tailwind-refactor: tailwind-refactor.
Files
Community Tailwind CSS Refactoring Best Practices
Comprehensive code quality refactoring guide for Tailwind CSS applications targeting v4. Contains 50 rules across 8 categories, prioritized by migration urgency. Every transformation preserves the existing look and feel — this skill is purely about cleaner code, modern syntax, and v4 compatibility.
Companion skills: Use tailwind-ui-refactor for visual design improvements and tailwind-responsive-ui for responsive layout patterns.
When to Apply
Before manual migration: Run npx @tailwindcss/upgrade first — it handles most configuration and renamed utility changes automatically. Then use this skill for patterns the automated tool does not cover.
Reference these guidelines when:
- Migrating a project from Tailwind CSS v3 to v4
- Cleaning up deprecated or renamed utility classes
- Consolidating verbose multi-class patterns
- Replacing arbitrary values with design tokens
- Removing
@applyoveruse in CSS files - Modernizing syntax to v4 conventions
Rule Categories by Priority
| Priority | Category | Impact | Prefix |
|---|---|---|---|
| 1 | Configuration Migration | CRITICAL | config- |
| 2 | Deprecated Utility Replacement | CRITICAL | dep- |
| 3 | Renamed Utility Updates | HIGH | rename- |
| 4 | Class Consolidation | HIGH | class- |
| 5 | Arbitrary Value Cleanup | MEDIUM-HIGH | arb- |
| 6 | Syntax Modernization | MEDIUM | syntax- |
| 7 | @apply & Architecture Cleanup | MEDIUM | arch- |
| 8 | Modern Feature Adoption | LOW-MEDIUM | adopt- |
Quick Reference
1. Configuration Migration (CRITICAL)
- `config-import-directive` - Replace @tailwind directives with @import
- `config-css-theme` - Migrate tailwind.config.js to CSS @theme
- `config-theme-function` - Replace theme() function with CSS variables
- `config-theme-inline` - Use @theme inline for non-utility design tokens
- `config-utility-directive` - Replace @layer utilities with @utility
- `config-postcss-plugin` - Update PostCSS plugin to @tailwindcss/postcss
- `config-content-autodetect` - Remove manual content configuration
- `config-custom-variant` - Migrate addVariant to @custom-variant
- `config-preflight-defaults` - Account for Preflight default changes in v4
2. Deprecated Utility Replacement (CRITICAL)
- `dep-opacity-modifiers` - Replace -opacity- with opacity modifiers (/50)
- `dep-flex-shorthand` - Replace flex-shrink/flex-grow with shrink/grow
- `dep-text-ellipsis` - Replace overflow-ellipsis with text-ellipsis
- `dep-decoration-utilities` - Replace decoration-slice/clone with box-decoration-*
- `dep-transform-composites` - Replace transform-none with individual resets
- `dep-transition-properties` - Update transition-[transform] to individual properties
3. Renamed Utility Updates (HIGH)
- `rename-shadow-scale` - Update shadow utilities to new scale
- `rename-blur-scale` - Update blur utilities to new scale
- `rename-rounded-scale` - Update border radius utilities to new scale
- `rename-ring-width` - Replace ring with ring-3 for v3 default
- `rename-gradient-utilities` - Replace bg-gradient- with bg-linear-
- `rename-outline-hidden` - Replace outline-none with outline-hidden
4. Class Consolidation (HIGH)
- `class-size-utility` - Replace matching w- h- with size-*
- `class-gap-over-space` - Prefer gap- over space-x/y- in flex/grid
- `class-inset-shorthand` - Replace top/right/bottom/left with inset-*
- `class-border-color-explicit` - Add explicit border color for v4 default change
- `class-ring-color-explicit` - Add explicit ring color for v4 default change
- `class-redundant-display` - Remove redundant display classes
- `class-hidden-priority` - Remove display overrides for hidden attribute
- `class-container-utility` - Replace container plugin config with @utility
5. Arbitrary Value Cleanup (MEDIUM-HIGH)
- `arb-hex-to-theme` - Replace arbitrary hex colors with theme tokens
- `arb-spacing-to-scale` - Replace arbitrary spacing with theme scale
- `arb-dynamic-classes` - Avoid dynamic class name construction
- `arb-breakpoint-to-theme` - Replace arbitrary breakpoints with @theme
- `arb-zindex-to-scale` - Replace arbitrary z-index with defined scale
6. Syntax Modernization (MEDIUM)
- `syntax-css-variable-parens` - Update CSS variable syntax from brackets to parentheses
- `syntax-variant-stacking` - Update variant stacking to left-to-right order
- `syntax-important-modifier` - Use trailing ! for important modifier
- `syntax-grid-arbitrary` - Use underscores in grid arbitrary values
- `syntax-gradient-preservation` - Reset gradient stops explicitly in variants
- `syntax-hover-media-query` - Account for hover variant media query wrapping
7. @apply & Architecture Cleanup (MEDIUM)
- `arch-apply-to-component` - Extract @apply blocks into framework components
- `arch-layer-to-utility` - Replace @layer components with @utility
- `arch-scoped-reference` - Use @reference for @apply in scoped styles
- `arch-safelist-to-source` - Replace safelist with @source inline()
- `arch-domain-composition` - Reserve Tailwind for primitives, compose for domain
8. Modern Feature Adoption (LOW-MEDIUM)
- `adopt-container-queries` - Use container queries instead of viewport breakpoints
- `adopt-not-variant` - Use not-* variant for negated conditions
- `adopt-in-variant` - Use in-* variant to simplify parent-state styling
- `adopt-field-sizing` - Use field-sizing-content for auto-resizing textareas
- `adopt-starting-variant` - Use starting variant for entry animations without JS
How to Use
Read individual reference files for detailed explanations and code examples:
- Section definitions - Category structure and impact levels
- Rule template - Template for adding new rules
Reference Files
| File | Description |
|---|---|
| references/_sections.md | Category definitions and ordering |
| assets/templates/_template.md | Template for new rules |
| metadata.json | Version and reference information |
Rule Title Here
1-3 sentences explaining WHY this matters. Focus on code quality implications — build failures, maintainability, or v4 compatibility.
Incorrect (what's wrong):
<!-- Bad code example with comment explaining the problem -->
<div class="old-pattern-here">Correct (what's right):
<!-- Good code example with minimal diff from incorrect -->
<div class="new-pattern-here">{
"version": "1.0.5",
"organization": "Community",
"technology": "Tailwind CSS",
"date": "February 2026",
"abstract": "Comprehensive code quality refactoring guide for Tailwind CSS applications targeting v4 migration and anti-pattern cleanup. Contains 50 rules across 8 categories, prioritized by migration urgency from critical (configuration migration, deprecated utility replacement) to incremental (modern feature adoption). Every transformation preserves the existing visual output — this skill focuses purely on cleaner code, modern v4 syntax, and elimination of code smells. Each rule includes incorrect vs. correct examples showing exact class-level transformations.",
"references": [
"https://tailwindcss.com/blog/tailwindcss-v4",
"https://tailwindcss.com/docs/upgrade-guide",
"https://tailwindcss.com/",
"https://spin.atomicobject.com/tailwind-css-anti-patterns/",
"https://stevekinney.com/courses/tailwind/tailwind-anti-patterns",
"https://www.faraazcodes.com/blog/tailwind-2025-best-practices"
]
}
Sections
This file defines all sections, their ordering, impact levels, and descriptions. The section ID (in parentheses) is the filename prefix used to group rules.
Note: Section impact levels indicate the maximum severity for that category. Individual rules within a section may have lower impact levels when the specific change is less urgent than the category's primary concern.
---
1. Configuration Migration (config)
Impact: CRITICAL Description: Migrating from JavaScript config to CSS-first @theme, @import, and @utility directives affects the entire project foundation and unlocks all v4 features.
2. Deprecated Utility Replacement (dep)
Impact: CRITICAL Description: Utilities removed in v4 (bg-opacity-*, flex-shrink-*, overflow-ellipsis) are silently purged — they produce no CSS output and cause broken styles that are hard to detect because the build succeeds without errors.
3. Renamed Utility Updates (rename)
Impact: HIGH Description: Utilities renamed in v4 (shadow-sm→shadow-xs, rounded-sm→rounded-xs, bg-gradient-*→bg-linear-*) produce incorrect styles if not updated.
4. Class Consolidation (class)
Impact: HIGH Description: Verbose multi-class patterns (w-8 h-8, space-y-4) can be replaced with single utilities (size-8, gap-4), reducing class count by 30-50%.
5. Arbitrary Value Cleanup (arb)
Impact: MEDIUM-HIGH Description: Inline hex colors, magic numbers, and dynamic class construction create design drift and break Tailwind's purge — replace with @theme tokens.
6. Syntax Modernization (syntax)
Impact: MEDIUM Description: Updating to v4 syntax patterns (CSS variable references, variant stacking order, important modifiers) prevents subtle bugs and improves consistency.
7. @apply & Architecture Cleanup (arch)
Impact: MEDIUM Description: Reducing @apply overuse, migrating @layer components to @utility, and extracting components improves maintainability without changing visual output.
8. Modern Feature Adoption (adopt)
Impact: LOW-MEDIUM Description: Leveraging new v4 features (container queries, not-* variant, in-* variant, field-sizing-content) produces cleaner markup for patterns that previously required workarounds.
Use Container Queries Instead of Viewport Breakpoints for Components
Viewport breakpoints couple a component's layout to the screen size, which breaks when the same component is placed in different containers (sidebar, modal, full-width). Container queries let the component respond to its own container's width, making it truly reusable. Tailwind v4 ships built-in container query support (@sm, @md, @lg, @xl, @max-*) with no plugin required.
Incorrect (what's wrong):
<!-- Component uses viewport breakpoints — breaks when placed in a narrow sidebar -->
<div class="md:flex md:gap-4">
<img class="md:w-1/3" />
<div class="md:w-2/3">...</div>
</div>Correct (what's right):
<!-- Component adapts to its container's width, works anywhere -->
<div class="@container">
<div class="@md:flex @md:gap-4">
<img class="@md:w-1/3" />
<div class="@md:w-2/3">...</div>
</div>
</div>Use field-sizing-content for Auto-Resizing Textareas
Auto-resizing textareas traditionally require JavaScript to measure scrollHeight and set inline styles on every input event. The field-sizing-content utility leverages a new CSS feature supported in Tailwind v4 that makes the element size itself to its content natively. No JavaScript, no layout thrashing, no edge cases with paste or undo.
Incorrect (what's wrong):
<!-- JavaScript resize handler — extra code, layout thrashing on every keystroke -->
<textarea
class="min-h-[80px] resize-none"
oninput="this.style.height='auto'; this.style.height=this.scrollHeight+'px'"
></textarea>Correct (what's right):
<!-- Single utility class — textarea grows with content automatically -->
<textarea class="field-sizing-content min-h-20"></textarea>The field-sizing-content utility also works on <input> elements, sizing them based on content width rather than a fixed size attribute.
When NOT to use this pattern:
field-sizing: contentis a bleeding-edge CSS property with limited browser support — as of early 2026, Firefox added support recently and Safari support may still be partial- For production apps that must support older browsers, keep the JavaScript auto-resize as a fallback and use
field-sizing-contentas a progressive enhancement - Check caniuse.com for current support before adopting
Use in-* Variant to Simplify Parent-State Styling
The group / group-* pattern requires adding an explicit group class to a parent element so children can react to its state. The in-* variant removes this requirement — it targets the nearest ancestor matching the condition implicitly. This means cleaner markup and one fewer class to maintain.
Incorrect (verbose — group is valid but more markup):
<!-- Must remember to add "group" class to parent -->
<div class="group rounded-lg p-4 hover:bg-gray-100">
<p class="group-hover:text-blue-500">Title</p>
<p class="group-hover:text-gray-600">Description</p>
</div>*Correct (concise — in- removes group boilerplate):**
<!-- No group class needed — in-* targets nearest matching ancestor -->
<div class="rounded-lg p-4 hover:bg-gray-100">
<p class="in-hover:text-blue-500">Title</p>
<p class="in-hover:text-gray-600">Description</p>
</div>When NOT to use this pattern:
groupis still fully supported and idiomatic in v4 —in-*is a convenience, not a deprecation- Use
group/name(named groups) when you need to target a specific ancestor in deeply nested structures —in-*always targets the nearest matching ancestor which may not be the intended one - Keep
groupwhen multiple nested components could match and you need explicit opt-in control over which ancestor is the target
Use not-* Variant for Negated Conditions
Tailwind v4 introduces the not-* variant to express negated conditions directly — "apply this style when the condition is NOT met." This is most valuable for pseudo-classes like :disabled, :focus, and :empty where the base-override pattern is awkward or verbose. For simple hover interactions, the standard base hover:override pattern remains idiomatic and often clearer.
Incorrect (verbose — base-override requires extra classes):
<!-- Must set disabled base styles, then override for enabled state -->
<button class="opacity-50 cursor-not-allowed enabled:opacity-100 enabled:cursor-pointer">
Submit
</button>*Correct (concise — not- expresses intent directly):**
<!-- Direct negation: style only when NOT disabled -->
<button class="not-disabled:opacity-100 not-disabled:cursor-pointer opacity-50 cursor-not-allowed">
Submit
</button>The not-* variant works with pseudo-classes, media queries, and supports queries:
<input class="not-focus:ring-0" />
<div class="not-supports-[backdrop-filter]:bg-gray-900">Fallback</div>
<div class="not-empty:p-4">Only pad when has content</div>When NOT to use this pattern:
- The standard
base hover:overridepattern (e.g.,opacity-75 hover:opacity-100) remains valid and preferred for simple hover interactions — do not rewrite it withnot-hover: - Avoid
not-*when the base-override approach is already clear and well-understood
Use starting Variant for Entry Animations Without JS
Entry animations for elements that appear on the page (popovers, dialogs, dynamically inserted content) traditionally require JavaScript to add/remove classes in a specific sequence. The starting variant maps to CSS @starting-style, which defines the initial state of an element before its first style update. Combined with the open variant for popovers and dialogs, you get smooth enter/exit animations purely in CSS.
Incorrect (what's wrong):
<!-- JavaScript orchestration needed to animate entry -->
<div id="popover" popover class="opacity-100 transition-opacity duration-300">
<!-- JS: element.classList.add('opacity-0') then requestAnimationFrame to remove -->Correct (what's right):
<!-- CSS-only entry animation using starting variant -->
<div popover class="open:opacity-100 starting:open:opacity-0 transition-opacity duration-300">
</div>Replace Arbitrary Breakpoints with @theme
Arbitrary breakpoints scatter responsive logic across files, making it impossible to see your responsive strategy at a glance. Defining breakpoints in @theme makes them searchable, reusable, and self-documenting. When a design changes its breakpoint boundary, you update one token instead of hunting through every template.
Incorrect (what's wrong):
<div class="min-[1200px]:flex min-[1200px]:gap-4">
<aside class="min-[1200px]:w-64">Repeated hardcoded breakpoint</aside>
</div>Correct (what's right):
@theme {
--breakpoint-wide: 1200px;
}<div class="wide:flex wide:gap-4">
<aside class="wide:w-64">Named breakpoint, one source of truth</aside>
</div>Avoid Dynamic Class Name Construction
Tailwind scans source files for complete class strings at build time. It does not execute JavaScript, so dynamically constructed class names are invisible to the compiler and 100% of those classes get purged from the production build. This is the single most common cause of "it works in dev but not production" bugs.
Incorrect (what's wrong):
// Tailwind cannot detect these classes — they WILL be purged
function Badge({ color, size }) {
return (
<span className={`bg-${color}-500 text-${size}`}>
Badge
</span>
);
}Correct (what's right):
// Complete literal strings that Tailwind can scan
const colorMap = {
red: 'bg-red-500',
blue: 'bg-blue-500',
green: 'bg-green-500',
};
const sizeMap = {
sm: 'text-sm',
base: 'text-base',
lg: 'text-lg',
};
function Badge({ color, size }) {
return (
<span className={`${colorMap[color]} ${sizeMap[size]}`}>
Badge
</span>
);
}For more complex variant logic, use CVA (Class Variance Authority) or a cn helper.
Replace Arbitrary Hex Colors with Theme Tokens
Arbitrary hex values bypass the design system, cannot be found by searching for token names, and create visual inconsistency across the project. Every hardcoded color is a potential deviation from brand guidelines that grows harder to maintain over time.
Incorrect (what's wrong):
<div class="bg-[#3b82f6] text-[#1e293b]">
<p class="border-[#e2e8f0]">Hardcoded hex values scattered everywhere</p>
</div>Correct (what's right):
/* In your main CSS file */
@theme {
--color-brand: #3b82f6;
--color-heading: #1e293b;
--color-border-subtle: #e2e8f0;
}<div class="bg-brand text-heading">
<p class="border-border-subtle">Theme tokens are searchable and consistent</p>
</div>Replace Arbitrary Spacing with Theme Scale
Tailwind v4 supports any integer spacing value dynamically from the --spacing variable (e.g., w-17, p-29, gap-13), so most arbitrary pixel values are unnecessary. Only use arbitrary spacing when the exact pixel value is externally mandated (e.g., matching a third-party embed dimension).
Incorrect (what's wrong):
<div class="p-[12px] mt-[28px] gap-[16px]">
<section class="mb-[32px] px-[24px]">Hardcoded pixel values</section>
</div>Correct (what's right):
<div class="p-3 mt-7 gap-4">
<section class="mb-8 px-6">Scale values from the spacing system</section>
</div>Important: Only replace arbitrary values that exactly match a scale value (default: N × 0.25rem). If the pixel value does not align with the spacing scale (e.g., p-[13px], mt-[27px]), replacing it with the nearest scale value will change the visual output. In such cases, either keep the arbitrary value or adjust the design to align with the scale first.
For truly custom spacing that repeats across the project, define it in @theme:
@theme {
--spacing-header: 72px;
--spacing-sidebar: 280px;
}<header class="h-header">Consistent named spacing</header>Replace Arbitrary z-index with a Defined Scale
Arbitrary z-index values create an escalation problem where developers keep adding higher numbers to "win" stacking battles. A defined scale prevents conflicts and documents the intended layering order for the entire application.
Note: z-index is not a theme-driven namespace in Tailwind v4, so @theme variables will not auto-generate z-* utilities. Instead, define CSS custom properties and reference them with the z-(--var) syntax, or create named utilities with @utility.
Incorrect (what's wrong):
<div class="z-[999]">Dropdown</div>
<div class="z-[9999]">Modal</div>
<div class="z-[99999]">Toast — the arms race continues</div>Correct (what's right):
Option A — CSS custom properties with z-(--var) syntax:
:root {
--z-dropdown: 50;
--z-modal: 100;
--z-toast: 150;
}<div class="z-(--z-dropdown)">Dropdown</div>
<div class="z-(--z-modal)">Modal</div>
<div class="z-(--z-toast)">Clear layering intent</div>Option B — named utilities with @utility:
@utility z-dropdown {
z-index: 50;
}
@utility z-modal {
z-index: 100;
}
@utility z-toast {
z-index: 150;
}<div class="z-dropdown">Dropdown</div>
<div class="z-modal">Modal</div>
<div class="z-toast">Clear layering intent</div>Extract @apply Blocks into Framework Components
@apply breaks the utility-first paradigm by creating inflexible style sets locked in CSS. It increases the CSS bundle size (every @apply duplicates the underlying declarations), cannot accept props or conditional logic, and makes overrides painful. Component extraction gives you the reuse of @apply with the full flexibility of utilities and framework features.
Incorrect (what's wrong):
/* Repeated @apply blocks for button variants */
.btn {
@apply inline-flex items-center gap-2 rounded-lg px-4 py-2 font-medium transition-colors;
}
.btn-primary {
@apply bg-blue-500 text-white hover:bg-blue-600;
}
.btn-secondary {
@apply bg-gray-100 text-gray-800 hover:bg-gray-200;
}Correct (what's right):
// Framework component with utility classes directly in markup
function Button({ variant = 'primary', children, ...props }) {
const styles = {
primary: 'bg-blue-500 text-white hover:bg-blue-600',
secondary: 'bg-gray-100 text-gray-800 hover:bg-gray-200',
};
return (
<button
className={`inline-flex items-center gap-2 rounded-lg px-4 py-2 font-medium transition-colors ${styles[variant]}`}
{...props}
>
{children}
</button>
);
}Reserve Tailwind for Primitive Components, Compose for Domain
Applying utility classes directly in domain-level components (checkout forms, user profiles, dashboards) creates massive duplication across feature modules. The same input styling, card layout, and button pattern get copy-pasted dozens of times. Instead, build a library of styled primitive components with Tailwind utilities, then compose those primitives in domain code without any utility classes.
Incorrect (what's wrong):
// Tailwind classes directly in domain components — duplicated everywhere
<form class="flex flex-col gap-4 rounded-lg bg-white p-6 shadow-md">
<input class="rounded-md border border-gray-300 px-3 py-2 focus:outline-hidden focus:ring-2 focus:ring-blue-500" />
<input class="rounded-md border border-gray-300 px-3 py-2 focus:outline-hidden focus:ring-2 focus:ring-blue-500" />
<button class="rounded-lg bg-blue-500 px-4 py-2 text-white hover:bg-blue-600">Submit</button>
</form>Correct (what's right):
// Styled primitives (Tailwind lives here)
function Card({ children }) {
return <div className="flex flex-col gap-4 rounded-lg bg-white p-6 shadow-md">{children}</div>;
}
function Input(props) {
return <input className="rounded-md border border-gray-300 px-3 py-2 focus:outline-hidden focus:ring-2 focus:ring-blue-500" {...props} />;
}
function Button({ children, ...props }) {
return <button className="rounded-lg bg-blue-500 px-4 py-2 text-white hover:bg-blue-600" {...props}>{children}</button>;
}
// Domain component — clean composition, zero utility classes
<Card>
<Input placeholder="Email" />
<Input placeholder="Password" type="password" />
<Button>Submit</Button>
</Card>Replace @layer components with @utility
Styles defined in @layer components do not get automatic variant support in Tailwind v4. The @utility directive generates all variants automatically, making your custom classes behave exactly like built-in Tailwind utilities. This is most useful for single-purpose utilities where variant support matters. For multi-property component compositions, consider framework components instead.
Incorrect (what's wrong):
/* @layer components — no variant support */
@layer components {
.scrollbar-hidden {
scrollbar-width: none;
-ms-overflow-style: none;
}
.scrollbar-hidden::-webkit-scrollbar {
display: none;
}
}<!-- hover:scrollbar-hidden won't work -->
<div class="scrollbar-hidden overflow-y-auto">Scrollable content</div>Correct (what's right):
/* @utility — full variant support */
@utility scrollbar-hidden {
scrollbar-width: none;
-ms-overflow-style: none;
&::-webkit-scrollbar {
display: none;
}
}<!-- All variants work automatically -->
<div class="hover:scrollbar-hidden overflow-y-auto">Show scrollbar, hide on hover</div>When NOT to use this pattern:
@applycannot be used inside@utilityblocks — use raw CSS properties only- Multi-property component compositions (cards, buttons, etc.) are better as framework components than as
@utilitydefinitions
Replace safelist with @source inline()
The v3 safelist configuration in tailwind.config.js has been replaced by @source inline() in v4. The new directive lives in your CSS alongside other configuration, uses glob-like patterns for matching multiple classes, and eliminates the need for a JavaScript config file for this purpose.
Incorrect (what's wrong):
// tailwind.config.js — v3 safelist
module.exports = {
safelist: ['bg-red-500', 'bg-blue-500', /^text-/],
}Correct (what's right):
/* In your main CSS file — v4 @source inline() */
@source inline("bg-red-500 bg-blue-500 text-{red,blue,green}-{100,500,900}");Use @reference for @apply in Scoped Styles
In Tailwind v4, scoped stylesheets (Vue <style scoped>, Svelte <style>) do not have access to Tailwind's generated theme by default. This means @apply will fail silently or produce incorrect output inside scoped blocks. The @reference directive imports the theme without duplicating any styles in the output. Even better, use CSS variables directly to avoid @apply entirely.
Incorrect (what's wrong):
<style scoped>
/* Broken in v4 — scoped styles can't see Tailwind's theme */
h1 {
@apply text-2xl font-bold text-red-500;
}
</style>Correct (what's right):
<style scoped>
@reference "../../app.css";
h1 {
@apply text-2xl font-bold text-red-500;
}
</style>Even better — use CSS variables directly:
<style scoped>
h1 {
color: var(--color-red-500);
font-size: var(--text-2xl);
font-weight: var(--font-weight-bold);
}
</style>Add Explicit Border Color for v4 Default Change
In Tailwind CSS v3, the border utility implicitly set the border color to gray-200. In v4, the default border color changed to currentColor, matching standard CSS behavior. This means any element using border without an explicit color class will now inherit the text color as its border color, which can produce a dramatically different appearance. Adding an explicit border-gray-200 preserves the original visual output without changing the design.
Incorrect (what's wrong):
<div class="border p-4">
Card with assumed gray border
</div>Correct (what's right):
<div class="border border-gray-200 p-4">
Card with explicit gray border
</div>Replace Container Plugin Config with @utility
Tailwind CSS v4 removed JavaScript-based container configuration from tailwind.config.js. The theme.container options for centering and padding no longer have any effect. Instead, define the container behavior directly in CSS using the @utility directive. This keeps all styling in CSS, eliminates the dependency on a JS config file for a purely visual concern, and gives you full control over the container's responsive behavior.
Incorrect (what's wrong):
// tailwind.config.js
module.exports = {
theme: {
container: {
center: true,
padding: '2rem',
},
},
};Correct (what's right):
@utility container {
margin-inline: auto;
padding-inline: 2rem;
}Prefer gap- over space-x/y- in Flex/Grid
In flex and grid layouts, gap-* uses the native CSS gap property, which handles dynamic children correctly without selector hacks. The space-x/y-* utilities work by applying margins via :not(:last-child) selectors (changed from v3's > * + *), which can cause unexpected spacing when children are conditionally shown or hidden. Note: space-* is still a fully supported utility in v4 — this is a quality improvement, not a required migration.
*Incorrect (space- with margin selectors):**
<div class="flex flex-col space-y-4">
<div>Item 1</div>
<div>Item 2</div>
<div>Item 3</div>
</div>*Correct (gap- with native CSS gap):**
<div class="flex flex-col gap-4">
<div>Item 1</div>
<div>Item 2</div>
<div>Item 3</div>
</div>When NOT to use this pattern:
gaponly works inside flex and grid containers — for non-flex/grid stacked children,space-*remains the correct approachspace-*is still valid when you need margin-based spacing for legacy layout reasons
Remove Display Overrides for hidden Attribute
In Tailwind CSS v3, display utilities like block or flex overrode the HTML hidden attribute, meaning <div hidden class="block"> would still be visible. This forced developers to write extra JavaScript to toggle both the hidden attribute and the display class simultaneously. In v4, the hidden attribute takes priority over display classes, so <div hidden class="flex"> correctly stays hidden. You can simplify show/hide logic by relying solely on the hidden attribute.
Incorrect (what's wrong):
<!-- v3 pattern: JS must toggle both hidden and display class -->
<div id="panel" hidden class="hidden">
Panel content
</div>
<script>
function showPanel() {
panel.removeAttribute('hidden');
panel.classList.remove('hidden');
panel.classList.add('flex');
}
</script>Correct (what's right):
<!-- v4 pattern: hidden attribute alone controls visibility -->
<div id="panel" hidden class="flex">
Panel content
</div>
<script>
function showPanel() {
panel.removeAttribute('hidden');
}
</script>Replace top/right/bottom/left with inset-*
When a positioned element sets all four sides to the same value, the inset-* utility replaces four separate classes with one. Similarly, inset-x-* replaces left-* and right-*, and inset-y-* replaces top-* and bottom-*. This reduces class noise significantly on overlay, modal, and full-bleed elements.
Incorrect (what's wrong):
<div class="absolute top-0 right-0 bottom-0 left-0">Full overlay</div>
<div class="absolute left-0 right-0">Horizontal stretch</div>
<div class="absolute top-0 bottom-0">Vertical stretch</div>Correct (what's right):
<div class="absolute inset-0">Full overlay</div>
<div class="absolute inset-x-0">Horizontal stretch</div>
<div class="absolute inset-y-0">Vertical stretch</div>Remove Redundant Display Classes
Several commonly used utility combinations include classes that simply restate the CSS default for that display mode. Removing these redundant classes reduces markup noise without changing any visual output. Common redundant defaults include: flex-row (default flex direction), flex-nowrap (default wrap behavior), grid-cols-1 (default single column), items-stretch (default align-items), justify-start (default justify-content), and text-left (default text alignment in LTR).
Incorrect (what's wrong):
<div class="flex flex-row">Side by side</div>
<div class="flex flex-nowrap">No wrap</div>
<div class="grid grid-cols-1">Single column</div>
<div class="flex items-stretch">Stretched items</div>
<div class="flex justify-start">Left-aligned items</div>Correct (what's right):
<div class="flex">Side by side</div>
<div class="flex">No wrap</div>
<div class="grid">Single column</div>
<div class="flex">Stretched items</div>
<div class="flex">Left-aligned items</div>When NOT to use this pattern:
- Only remove these classes when they appear without a variant prefix
- Classes like
md:flex-row,lg:items-stretch,hover:justify-start,rtl:text-leftserve a real purpose as responsive or conditional overrides and must be kept - Example:
flex flex-col md:flex-row—md:flex-rowis NOT redundant here - Some teams prefer explicit defaults for code readability — if your convention is to document layout intent with explicit classes (e.g.,
flex flex-rowto distinguish from aflex flex-colsibling), keep them
Add Explicit Ring Color for v4 Default Change
In Tailwind CSS v3, the ring utility implicitly set the ring color to blue-500 at 50% opacity. In v4, the default ring color changed to currentColor, matching standard CSS behavior. This means any element using ring without an explicit color class will now inherit the text color as its ring color, which can produce a dramatically different appearance — especially on focus states.
Incorrect (what's wrong):
<input class="ring focus:ring-2" />
<button class="focus:ring">Assumed blue ring</button>Correct (what's right):
<input class="ring ring-blue-500/50 focus:ring-2 focus:ring-blue-500/50" />
<button class="focus:ring focus:ring-blue-500/50">Explicit blue ring</button>If your project uses a different brand color for focus rings, replace blue-500/50 with the appropriate theme token.
Replace Matching w- h- with size-*
When an element has identical width and height values, the size-* utility replaces both w-* and h-* in a single class. This cuts the class count in half for square elements like avatars, icons, and equal-dimension containers. Only apply this consolidation when the width and height values are exactly the same.
Incorrect (what's wrong):
<img class="w-8 h-8" src="avatar.jpg" />
<div class="w-full h-full">Overlay</div>
<div class="w-screen h-screen">Full viewport</div>Correct (what's right):
<img class="size-8" src="avatar.jpg" />
<div class="size-full">Overlay</div>
<div class="size-screen">Full viewport</div>Remove Manual Content Configuration
Tailwind CSS v4 automatically detects your template files by scanning the project directory. The manual content array in the config is no longer needed and should be removed. Keeping a manual list can actually cause issues — files outside the listed globs will be silently missed even though auto-detection would have found them.
Incorrect (what's wrong):
// tailwind.config.js
module.exports = {
content: ['./src/**/*.{html,js,tsx}', './public/index.html'],
};Correct (what's right):
Remove the content configuration entirely. Tailwind v4 auto-detection respects .gitignore and ignores binary files automatically.
For edge cases where you need to include files outside the project root — such as a monorepo dependency — use the @source directive in your CSS:
@import "tailwindcss";
@source "../node_modules/@my-company/ui-lib";Migrate tailwind.config.js to CSS @theme
Tailwind CSS v4 replaces the JavaScript configuration file with a CSS-native @theme directive. Moving your design tokens into CSS eliminates the JS build dependency, enables standard CSS tooling to understand your tokens, and makes them available as CSS custom properties throughout your project without any extra configuration.
Incorrect (what's wrong):
// tailwind.config.js
module.exports = {
theme: {
extend: {
colors: {
brand: '#3b82f6',
surface: '#f8fafc',
},
fontFamily: {
display: ['Inter', 'sans-serif'],
},
spacing: {
18: '4.5rem',
},
},
},
};Correct (what's right):
@import "tailwindcss";
@theme {
--color-brand: #3b82f6;
--color-surface: #f8fafc;
--font-display: "Inter", sans-serif;
--spacing-18: 4.5rem;
}If you have a large or complex config that cannot be migrated all at once, use the escape hatch to reference a legacy config file:
@config "../../tailwind.config.js";Migrate addVariant to @custom-variant
Tailwind CSS v4 replaces the JavaScript addVariant plugin API with a CSS-native @custom-variant directive. This eliminates the need for a JS plugin file just to register custom variants, keeping your variant definitions alongside the rest of your CSS configuration.
Incorrect (what's wrong):
// tailwind.config.js
const plugin = require('tailwindcss/plugin');
module.exports = {
plugins: [
plugin(function ({ addVariant }) {
addVariant('hocus', ['&:hover', '&:focus']);
addVariant('group-hocus', [':merge(.group):hover &', ':merge(.group):focus &']);
}),
],
};Correct (what's right):
@custom-variant hocus (&:hover, &:focus);
@custom-variant group-hocus (:merge(.group):hover &, :merge(.group):focus &);You can then use these variants directly in your templates:
<button class="hocus:underline group-hocus:text-brand">Click me</button>Replace @tailwind Directives with @import
Tailwind CSS v4 removes the @tailwind directive entirely. The new CSS-first configuration model uses a single @import "tailwindcss" statement that replaces all three legacy directives. Without this change, your project will fail to compile on v4.
Incorrect (what's wrong):
@tailwind base;
@tailwind components;
@tailwind utilities;Correct (what's right):
@import "tailwindcss";If your project needs prefixed classes to avoid collisions with existing CSS, use the prefix option:
@import "tailwindcss" prefix(tw);This produces classes like tw:flex, tw:mt-4, and tw:text-red-500 instead of their unprefixed equivalents.
Update PostCSS Plugin to @tailwindcss/postcss
Tailwind CSS v4 ships its PostCSS integration as a separate package called @tailwindcss/postcss. The old tailwindcss package no longer exports a PostCSS plugin, so your build pipeline will break immediately unless the plugin reference is updated.
Incorrect (what's wrong):
// postcss.config.js
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};Correct (what's right):
// postcss.config.js
module.exports = {
plugins: {
"@tailwindcss/postcss": {},
},
};For Vite projects, use the dedicated Vite plugin instead of PostCSS for better performance:
// vite.config.js
import tailwindcss from "@tailwindcss/vite";
export default {
plugins: [tailwindcss()],
};Account for Preflight Default Changes in v4
Tailwind v4 changed several Preflight (base reset) defaults that can cause visible regressions after migration. These are silent — no build errors, no warnings — but users will notice the differences immediately.
Placeholder color
In v3, placeholder text used gray-400. In v4, placeholders use the current text color at 50% opacity.
Incorrect (what's wrong):
<!-- Assumes v3 gray-400 placeholder color -->
<input class="text-gray-900" placeholder="Search..." />Correct (what's right):
<!-- Explicitly set placeholder color to match v3 appearance -->
<input class="text-gray-900 placeholder-gray-400" placeholder="Search..." />Button cursor
In v3, Preflight set cursor: pointer on all buttons. In v4, buttons use the browser default (cursor: default).
Incorrect (what's wrong):
<!-- Assumes buttons have pointer cursor from Preflight -->
<button class="bg-blue-500 text-white px-4 py-2 rounded">Submit</button>Correct (what's right):
<!-- Explicitly add cursor-pointer to buttons -->
<button class="bg-blue-500 text-white px-4 py-2 rounded cursor-pointer">Submit</button>Or apply globally in your CSS:
@layer base {
button, [role="button"] {
cursor: pointer;
}
}Dialog centering
In v3, Preflight centered <dialog> elements with margin: auto. In v4, dialogs use the browser default positioning.
Incorrect (what's wrong):
<!-- Assumes dialog is auto-centered by Preflight -->
<dialog class="rounded-lg p-6 shadow-xl">
<p>Modal content</p>
</dialog>Correct (what's right):
<!-- Explicitly center the dialog -->
<dialog class="mx-auto my-auto rounded-lg p-6 shadow-xl">
<p>Modal content</p>
</dialog>Replace theme() Function with CSS Variables
The theme() function is deprecated in Tailwind CSS v4. All design tokens defined in @theme are now exposed as standard CSS custom properties, so you should reference them with var() instead. This aligns Tailwind with native CSS tooling and eliminates a proprietary function from your stylesheets.
Incorrect (what's wrong):
.banner {
background-color: theme(colors.red.500);
width: theme(screens.xl);
font-family: theme(fontFamily.sans);
}Correct (what's right):
.banner {
background-color: var(--color-red-500);
width: var(--breakpoint-xl);
font-family: var(--font-sans);
}Note: CSS variables cannot be used inside @media condition values. In media queries, use theme() with the CSS variable name syntax as a temporary workaround:
@media (width >= theme(--breakpoint-xl)) {
/* styles */
}Use @theme inline for Non-Utility Design Tokens
Tailwind v4.1 introduced @theme inline for CSS variables that should be available in your CSS via var() but should NOT generate utility classes. This is useful for internal design tokens like animation timing, component-specific values, or intermediate calculations that would pollute the utility namespace.
Incorrect (what's wrong):
@theme {
--color-brand: #3b82f6;
--animation-duration-fast: 150ms;
--animation-duration-normal: 300ms;
--sidebar-width: 280px;
}This generates utility classes like animation-duration-fast, sidebar-width — classes nobody should use directly.
Correct (what's right):
/* Public tokens — generate utility classes */
@theme {
--color-brand: #3b82f6;
}
/* Internal tokens — available via var() but no utility classes */
@theme inline {
--animation-duration-fast: 150ms;
--animation-duration-normal: 300ms;
--sidebar-width: 280px;
}/* Reference internal tokens in your CSS */
.sidebar {
width: var(--sidebar-width);
transition: transform var(--animation-duration-normal);
}Replace @layer utilities with @utility
In Tailwind CSS v4, custom utilities defined with @layer utilities will not be recognized by the variant system. The new @utility directive registers each utility so that Tailwind automatically generates all variants — hover, focus, responsive breakpoints, dark mode, and every other modifier — without any additional configuration.
Incorrect (what's wrong):
@layer utilities {
.tab-4 {
tab-size: 4;
}
}Correct (what's right):
@utility tab-4 {
tab-size: 4;
}With @utility, you can immediately use hover:tab-4, md:tab-4, dark:tab-4, and any other variant combination in your templates without writing extra CSS.
Replace decoration-slice/clone with box-decoration-*
The decoration-slice and decoration-clone utilities have been removed in Tailwind CSS v4. They are replaced by box-decoration-slice and box-decoration-clone, which more clearly map to the underlying box-decoration-break CSS property and avoid naming collisions with text decoration utilities.
Incorrect (what's wrong):
<span class="decoration-slice bg-gradient-to-r from-indigo-500 to-pink-500">
Multi-line highlighted text
</span>
<span class="decoration-clone bg-blue-500 px-2">
Cloned box decoration
</span>Correct (what's right):
<span class="box-decoration-slice bg-gradient-to-r from-indigo-500 to-pink-500">
Multi-line highlighted text
</span>
<span class="box-decoration-clone bg-blue-500 px-2">
Cloned box decoration
</span>Replace flex-shrink and flex-grow with shrink and grow
The flex-shrink-* and flex-grow-* utilities were deprecated in Tailwind v3.3 in favor of the shorter shrink-* and grow-* aliases. While both forms worked in v3, Tailwind CSS v4 removes the flex- prefixed versions entirely. The classes are silently purged and produce no CSS output, resulting in broken layouts that are difficult to detect because the build itself succeeds without errors.
Incorrect (what's wrong):
<div class="flex">
<div class="flex-shrink-0">Logo</div>
<div class="flex-grow">Content</div>
<div class="flex-shrink flex-grow-0">Sidebar</div>
</div>Correct (what's right):
<div class="flex">
<div class="shrink-0">Logo</div>
<div class="grow">Content</div>
<div class="shrink grow-0">Sidebar</div>
</div>Replace -opacity- Utilities with Opacity Modifiers
Tailwind CSS v4 removes all six standalone opacity utility families: bg-opacity-*, text-opacity-*, border-opacity-*, divide-opacity-*, ring-opacity-*, and placeholder-opacity-*. These utilities relied on CSS custom properties injected alongside the color utility and no longer exist in the v4 codebase. The modern replacement is the opacity modifier syntax (/), which is more concise, composable with arbitrary values, and works consistently across all color utilities.
Incorrect (what's wrong):
<div class="bg-blue-500 bg-opacity-50">
<p class="text-red-600 text-opacity-75">
<div class="border border-gray-300 border-opacity-25">
<div class="divide-y divide-slate-200 divide-opacity-50">
<div class="ring ring-indigo-500 ring-opacity-30">
<input class="placeholder-gray-400 placeholder-opacity-60" placeholder="Search...">Correct (what's right):
<div class="bg-blue-500/50">
<p class="text-red-600/75">
<div class="border border-gray-300/25">
<div class="divide-y divide-slate-200/50">
<div class="ring ring-indigo-500/30">
<input class="placeholder-gray-400/60" placeholder="Search...">Replace overflow-ellipsis with text-ellipsis
The overflow-ellipsis utility has been removed in Tailwind CSS v4. It was renamed to text-ellipsis to better reflect that it sets the text-overflow CSS property. Any remaining usage of overflow-ellipsis will cause a build failure in v4. Additionally, consider using the truncate utility as a single-class shorthand that combines text-ellipsis, overflow-hidden, and whitespace-nowrap.
Incorrect (what's wrong):
<p class="overflow-ellipsis overflow-hidden whitespace-nowrap">
This text will be truncated with an ellipsis when it overflows.
</p>Correct (what's right):
<p class="text-ellipsis overflow-hidden whitespace-nowrap">
This text will be truncated with an ellipsis when it overflows.
</p>
<!-- Or use the truncate shorthand which applies all three at once -->
<p class="truncate">
This text will be truncated with an ellipsis when it overflows.
</p>Replace transform-none with Individual Transform Resets
Tailwind CSS v4 uses individual CSS transform properties (scale, rotate, translate) instead of the composite transform property. This means transform-none no longer resets individual transforms like scale-150 or rotate-45, because they are applied via separate CSS properties that transform: none does not affect. You must reset each transform property individually using scale-none, rotate-none, or translate-none.
Incorrect (what's wrong):
<!-- transform-none does NOT reset scale in v4 because scale uses its own CSS property -->
<div class="scale-150 hover:transform-none">
This element stays scaled on hover in v4
</div>
<div class="rotate-45 translate-x-4 md:transform-none">
This element stays rotated and translated on md in v4
</div>Correct (what's right):
<!-- Reset each individual transform property -->
<div class="scale-150 hover:scale-none">
This element resets scale on hover
</div>
<div class="rotate-45 translate-x-4 md:rotate-none md:translate-x-0">
This element resets rotation and translation on md
</div>Update transition-[transform] to Individual Properties
Tailwind CSS v4 uses individual CSS transform properties (scale, rotate, translate) instead of the composite transform property. When you specify transition-[transform] or include transform in a transition property list, it only animates the composite transform property -- but v4 utilities like scale-*, rotate-*, and translate-* set their own individual CSS properties, which will not be covered by that transition. You must reference the specific individual properties you want to animate.
Incorrect (what's wrong):
<!-- transform is not the property being animated in v4 -->
<div class="transition-[opacity,transform] hover:scale-150 hover:opacity-0">
Scale transition will not animate in v4
</div>
<button class="transition-transform hover:rotate-12">
Rotation transition will not animate in v4
</button>Correct (what's right):
<!-- Reference the individual CSS properties that v4 actually uses -->
<div class="transition-[opacity,scale] hover:scale-150 hover:opacity-0">
Scale transition animates correctly in v4
</div>
<button class="transition-[rotate] hover:rotate-12">
Rotation transition animates correctly in v4
</button>Update Blur Utilities to New Scale
Tailwind CSS v4 shifted the blur scale down by one step, mirroring the same pattern as shadow utilities. Keeping the v3 class names results in a stronger blur than intended, which can make text unreadable or backgrounds overly obscured.
| v3 class | v4 class |
|---|---|
blur-sm | blur-xs |
blur | blur-sm |
backdrop-blur-sm | backdrop-blur-xs |
backdrop-blur | backdrop-blur-sm |
Incorrect (what's wrong):
<div class="blur-sm">Subtle blur</div>
<div class="blur">Default blur</div>
<div class="backdrop-blur-sm">Subtle backdrop</div>
<div class="backdrop-blur">Default backdrop</div>Correct (what's right):
<div class="blur-xs">Subtle blur</div>
<div class="blur-sm">Default blur</div>
<div class="backdrop-blur-xs">Subtle backdrop</div>
<div class="backdrop-blur-sm">Default backdrop</div>Replace bg-gradient- with bg-linear-
Tailwind CSS v4 renamed all bg-gradient-to-* utilities to bg-linear-to-* to align with the CSS specification and to make room for new gradient types. The old bg-gradient-* classes no longer exist in v4 and will silently produce no output. In addition to the rename, v4 introduces bg-conic-* and bg-radial-* utilities, angle-based syntax like bg-linear-45, and color space modifiers such as bg-linear-to-r/oklch for perceptually smoother gradients.
Incorrect (what's wrong):
<div class="bg-gradient-to-r from-blue-500 to-purple-600">
Gradient banner
</div>Correct (what's right):
<div class="bg-linear-to-r from-blue-500 to-purple-600">
Gradient banner
</div>Replace outline-none with outline-hidden
In Tailwind CSS v4, outline-none now sets outline-style: none, which completely removes the focus outline for all users, including those navigating with a keyboard. This is an accessibility regression because Windows High Contrast Mode relies on visible outlines to indicate focus. The new outline-hidden utility uses a transparent outline that is invisible under normal rendering but remains visible in High Contrast Mode, preserving accessibility while achieving the same visual result.
Incorrect (what's wrong):
<button class="focus:outline-none focus:ring-2 focus:ring-blue-500">
Click me
</button>Correct (what's right):
<button class="focus:outline-hidden focus:ring-2 focus:ring-blue-500">
Click me
</button>Replace ring with ring-3 for v3 Default
In Tailwind CSS v3, the bare ring utility generated a 3px ring. In v4, ring now produces a 1px ring, which is visually much thinner and may be imperceptible on some displays. If your design relied on the v3 default ring width, you must explicitly use ring-3 to preserve the same appearance. Additionally, the default ring color changed from blue-500 to currentColor, so verify ring colors are explicitly set where needed.
Incorrect (what's wrong):
<button class="focus:ring ring-blue-500">
Submit
</button>Correct (what's right):
<button class="focus:ring-3 ring-blue-500">
Submit
</button>Update Border Radius Utilities to New Scale
Tailwind CSS v4 shifted the border radius scale down by one step, just like shadow and blur utilities. The bare rounded class now maps to a smaller radius value, and rounded-sm becomes the new smallest step. Without updating, all rounded corners in your project render larger than designed.
| v3 class | v4 class |
|---|---|
rounded-sm | rounded-xs |
rounded | rounded-sm |
Incorrect (what's wrong):
<button class="rounded-sm px-4 py-2">Subtle rounding</button>
<div class="rounded p-4">Default rounding</div>Correct (what's right):
<button class="rounded-xs px-4 py-2">Subtle rounding</button>
<div class="rounded-sm p-4">Default rounding</div>Update Shadow Utilities to New Scale
Tailwind CSS v4 shifted the entire shadow scale down by one step, inserting a new smallest size at each end. If you keep the v3 class names, every shadow in your project renders one size larger than intended. The same shift applies to both shadow-* and drop-shadow-* utilities.
| v3 class | v4 class |
|---|---|
shadow-sm | shadow-xs |
shadow | shadow-sm |
drop-shadow-sm | drop-shadow-xs |
drop-shadow | drop-shadow-sm |
Incorrect (what's wrong):
<div class="shadow-sm">Subtle card shadow</div>
<div class="shadow">Default card shadow</div>
<div class="drop-shadow-sm">Subtle image shadow</div>
<div class="drop-shadow">Default image shadow</div>Correct (what's right):
<div class="shadow-xs">Subtle card shadow</div>
<div class="shadow-sm">Default card shadow</div>
<div class="drop-shadow-xs">Subtle image shadow</div>
<div class="drop-shadow-sm">Default image shadow</div>Update CSS Variable Syntax from Brackets to Parentheses
Tailwind v4 changed the arbitrary CSS variable syntax from square brackets to parentheses. Square brackets still work for non-variable arbitrary values like bg-[#ff0000], but CSS custom properties now use the parenthesis form. Using the old syntax may produce warnings and will eventually be removed.
Incorrect (what's wrong):
<!-- v3 bracket syntax for CSS variables -->
<div class="bg-[--brand-color] text-[--heading-color] border-[--border-color]">
Old variable syntax
</div>Correct (what's right):
<!-- v4 parenthesis syntax for CSS variables -->
<div class="bg-(--brand-color) text-(--heading-color) border-(--border-color)">
New variable syntax
</div>Reset Gradient Stops Explicitly in Variants
In Tailwind v3, gradient color stops were reset when a variant changed the from-* value. In v4, gradient stops are preserved across variants, meaning a to-* value set in the base state will persist into dark: or hover: variants. You must explicitly set all gradient stops for each variant to avoid unexpected color combinations.
Incorrect (what's wrong):
<!-- to-yellow-400 persists into dark mode — unintended gradient -->
<div class="bg-linear-to-r from-red-500 to-yellow-400 dark:from-blue-500">
Yellow leaks into dark mode gradient
</div>Correct (what's right):
<!-- Explicitly set all stops per variant -->
<div class="bg-linear-to-r from-red-500 to-yellow-400 dark:from-blue-500 dark:to-teal-400">
Each variant fully defines its gradient
</div>To clear a via-* stop in a variant, use dark:via-none.
Use Underscores in Grid Arbitrary Values
Tailwind v4 no longer converts commas to spaces in arbitrary values. Since grid template definitions use spaces to separate track sizes, you must use underscores (which Tailwind converts to spaces) instead of commas. This is consistent with how other arbitrary values handle spaces throughout the framework.
Incorrect (what's wrong):
<!-- v3 comma syntax — v4 no longer converts commas to spaces -->
<div class="grid grid-cols-[max-content,1fr,auto]">
Broken grid in v4
</div>Correct (what's right):
<!-- v4 underscore syntax — underscores become spaces -->
<div class="grid grid-cols-[max-content_1fr_auto]">
Working grid in v4
</div>Account for Hover Variant Media Query Wrapping
Tailwind v4 wraps the hover: variant inside @media (hover: hover), which means hover styles only apply on devices that actually support hover (mouse/trackpad). This is the correct behavior — hover effects on touch devices were always a UX bug that caused "sticky hover" states. Be aware of this change when testing on mobile devices.
Incorrect (hover-only — no touch feedback):
<!-- hover: now only applies on devices with hover support (correct for most cases) -->
<button class="hover:bg-blue-600">
Will not show hover feedback on touch-only devices in v4
</button>Correct (hover + touch feedback):
<!-- Use active: for touch feedback alongside hover: for pointer devices -->
<button class="hover:bg-blue-600 active:bg-blue-700">
Hover feedback on desktop, press feedback on touch
</button>If you genuinely need hover behavior on all devices (rare), restore v3 behavior with:
@custom-variant hover (&:hover);When NOT to apply this rule:
- Standard
hover:usage is NOT incorrect in v4 — the default behavior is correct for most cases - Only add
active:variants when touch feedback is important for the interaction (buttons, links, interactive cards) - Do not blanket-add
active:to everyhover:class in a codebase
Use Trailing ! for Important Modifier
Tailwind v4 moves the !important modifier from a prefix to a suffix position. The v3 prefix syntax (!flex) still works but is deprecated and will be removed in a future version. The suffix form (flex!) is the canonical v4 style and reads more naturally as "flex, importantly."
Incorrect (what's wrong):
<!-- v3 prefix ! syntax (deprecated) -->
<div class="!flex !bg-red-500 !text-white">
Prefix important
</div>Correct (what's right):
<!-- v4 suffix ! syntax -->
<div class="flex! bg-red-500! text-white!">
Suffix important
</div>Update Variant Stacking to Left-to-Right Order
Tailwind v4 applies stacked variants left-to-right, matching CSS nesting order. In v3, variants applied right-to-left, which was counterintuitive. The new order reads naturally: each variant wraps the next, just like reading a sentence.
Incorrect (what's wrong):
<!-- v3 right-to-left: "first child, then direct children" -->
<ul class="first:*:pt-0 last:*:pb-0">
<li>Item</li>
</ul>Correct (what's right):
<!-- v4 left-to-right: "direct children, then first/last" — reads naturally -->
<ul class="*:first:pt-0 *:last:pb-0">
<li>Item</li>
</ul>Related skills
FAQ
What does tailwind-refactor do?
tailwind-refactor: A skill for development. This provides functionality for development workflows.
When should I use tailwind-refactor?
When you need to use tailwind-refactor for development tasks, or when tailwind-refactor: a skill for development. this provides functionality for development workflows.
What are the main capabilities?
tailwind-refactor.