
Tailwind V4 Best Practices
- 57 installs
- Updated April 17, 2026
- jenishshrestha/ai-skills
Production-grade Tailwind CSS v4 with design-system thinking: semantic design tokens, OKLCH colors, @theme configuration, and complete-class-name patterns.
About
Guides writing Tailwind v4 with semantic tokens over direct colors, the built-in scale over arbitrary values, and @theme-based design tokens for theme switching. A developer uses it when writing or reviewing Tailwind CSS or migrating from v3 to v4.
- Semantic design tokens and OKLCH colors over hardcoded palette values
- @theme configuration and complete-class-name rules for maintainable styling
Tailwind V4 Best Practices by the numbers
- 57 all-time installs (skills.sh)
- +2 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #1,238 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/jenishshrestha/ai-skills --skill tailwind-v4-best-practicesAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 57 |
|---|---|
| Last updated | April 17, 2026 |
| Repository | jenishshrestha/ai-skills ↗ |
What it does
Production-grade Tailwind CSS v4 with design-system thinking: semantic design tokens, OKLCH colors, @theme configuration, and complete-class-name patterns.
Files
Tailwind CSS v4 Best Practices
Production-grade Tailwind CSS v4 with a focus on design system thinking, maintainability, and performance.
Core Principles
1. Design Tokens, Not Direct Colors
Use semantic tokens instead of Tailwind's default color palette. This enables theme switching without touching components.
<!-- ❌ Direct colors — breaks when you rebrand -->
<button class="bg-blue-500 text-white hover:bg-blue-600">
<span class="text-red-600">Error</span>
<!-- ✅ Semantic tokens — theme-aware -->
<button class="bg-primary text-primary-foreground hover:bg-primary-hover">
<span class="text-error">Error</span>2. Semantic Utilities Over Arbitrary Values
Use the built-in scale. Reserve arbitrary values for true one-offs.
<!-- ❌ Arbitrary when a utility exists -->
<h1 class="text-[20px]">
<div class="p-[16px] gap-[24px]">
<!-- ✅ Use the scale -->
<h1 class="text-2xl">
<div class="p-4 gap-6">3. Complete Class Names Only
Tailwind's compiler needs full class names at build time. Never construct them dynamically.
// ❌ Tailwind can't detect these
<div className={`bg-${color}-500`}>
// ✅ Complete class names or CSS variables
<div className={color === 'blue' ? 'bg-blue-500' : 'bg-red-500'}>
<div className="bg-[var(--dynamic-color)]">4. Avoid @apply
Tailwind v4 discourages @apply. Use utility classes directly in templates, or use theme() in CSS when you need custom component classes.
/* ❌ v3 pattern */
.button { @apply px-4 py-2 bg-blue-500 text-white rounded; }
/* ✅ v4 — use theme() if you need CSS */
.button {
padding-inline: theme(spacing.4);
background-color: theme(colors.primary);
}@theme Configuration
Tailwind v4 moves config from JavaScript to CSS. Define design tokens with @theme:
@import "tailwindcss";
@theme {
/* Semantic colors using OKLCH */
--color-primary: oklch(0.55 0.25 250);
--color-primary-hover: oklch(0.50 0.25 250);
--color-primary-foreground: oklch(0.98 0 0);
--color-error: oklch(0.60 0.22 25);
--color-success: oklch(0.65 0.18 145);
/* Surface colors */
--color-background: oklch(1.0 0 0);
--color-foreground: oklch(0.20 0.01 250);
--color-surface: oklch(0.98 0.01 250);
--color-border: oklch(0.90 0.01 250);
/* Typography */
--font-family-heading: "Inter", sans-serif;
--font-family-body: "Inter", sans-serif;
--font-family-mono: "Fira Code", monospace;
}For full token architecture (Primitive → Semantic → Component layers), see references/design-tokens.md.
OKLCH Colors
Tailwind v4 defaults to OKLCH for better perceptual uniformity.
oklch(lightness chroma hue / alpha)- Lightness: 0 (black) to 1 (white)
- Chroma: 0 (gray) to ~0.4 (vivid)
- Hue: 0-360 degrees
- Alpha: 0-1 (optional)
--color-brand: oklch(0.55 0.25 270); /* vivid purple */
--color-subtle: oklch(0.96 0.02 250); /* muted gray */
--color-overlay: oklch(0.0 0 0 / 0.5); /* translucent black */v3 → v4 Class Name Renames
v3 → v4
───────────────────────────────
bg-gradient-to-r → bg-linear-to-r
bg-gradient-to-br → bg-linear-to-br
flex-shrink-0 → shrink-0
flex-grow → grow
decoration-clone → box-decoration-clone
decoration-slice → box-decoration-sliceRun the automated upgrade: npx @tailwindcss/upgrade
Vite Setup (Fastest)
import tailwindcss from '@tailwindcss/vite'
export default {
plugins: [tailwindcss()],
}v4 auto-detects content — no need to configure content paths. Unused utilities are tree-shaken automatically in production.
References
Load only what you need for the current task:
| Reference | Load When |
|---|---|
| design-tokens.md | Setting up token architecture, component tokens, status color patterns |
| theming.md | Dark mode, runtime theme switching, brand variants, user preference detection |
| advanced-patterns.md | Animations, grid layouts, container queries, custom utilities, plugins, performance |
| migration.md | Migrating a project from Tailwind v3 to v4 |
{
"skill_name": "tailwind-v4-best-practices",
"evals": [
{
"id": 1,
"name": "direct-color-violation",
"prompt": "I'm styling a button with bg-blue-500 text-white hover:bg-blue-600 and an error message with text-red-600. Is this correct for our project?",
"expected_output": "Should flag direct color usage and recommend semantic tokens: bg-primary, text-primary-foreground, hover:bg-primary-hover, text-error. Should explain why semantic tokens enable theming without changing components."
},
{
"id": 2,
"name": "dynamic-class-construction",
"prompt": "I have a component that sets className={`bg-${status}-500`} where status is 'green', 'red', or 'yellow'. It works in dev but some colors are missing in production. Why?",
"expected_output": "Should identify the dynamic class construction as the root cause — Tailwind can't detect partial class names at build time. Should recommend either a lookup object with complete class names, or CSS variables with semantic tokens like bg-status-success."
},
{
"id": 3,
"name": "theme-setup",
"prompt": "I'm setting up a new Tailwind v4 project. I still have a tailwind.config.js from v3 with custom colors defined as hex values. How do I migrate?",
"expected_output": "Should recommend moving config to @theme in CSS, converting hex to OKLCH, using @import 'tailwindcss' instead of @tailwind directives, and running npx @tailwindcss/upgrade. Should point to migration.md reference for the full checklist."
},
{
"id": 4,
"name": "design-token-architecture",
"prompt": "Our app has 50+ components and we keep having color inconsistencies. How should we organize our design tokens in Tailwind v4?",
"expected_output": "Should recommend the three-layer token architecture (Primitive → Semantic → Component). Should point to design-tokens.md reference. Should explain that components never reference primitives directly."
},
{
"id": 5,
"name": "apply-usage",
"prompt": "I'm using @apply px-4 py-2 bg-primary text-white rounded in a .button class. My coworker says this is wrong in v4. Is it?",
"expected_output": "Should explain that v4 discourages @apply. Recommend either using utility classes directly in templates, or using theme() function in CSS. Should show the theme() alternative."
}
]
}
Advanced Patterns
Table of Contents
1. Container Queries 2. CSS Grid Layouts 3. Animations 4. Custom Utilities 5. Plugin Development 6. Performance
Container Queries
Component-based responsive design — responds to container width, not viewport.
<div class="@container">
<div class="@sm:p-6 @md:flex @md:gap-6 @lg:grid @lg:grid-cols-2">
<div class="@md:flex-1">Content</div>
<div class="@md:flex-1">Sidebar</div>
</div>
</div>Named containers for targeted queries:
@layer components {
.product-card {
container-type: inline-size;
container-name: product-card;
}
.product-card__content {
display: flex;
flex-direction: column;
gap: theme(spacing.4);
}
@container product-card (min-width: 400px) {
.product-card__content {
flex-direction: row;
align-items: center;
}
}
@container product-card (min-width: 600px) {
.product-card__content {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
}
}
}CSS Grid Layouts
Named Grid Areas
@layer components {
.dashboard-layout {
display: grid;
grid-template-areas:
'header header header'
'sidebar main aside'
'footer footer footer';
grid-template-columns: 250px 1fr 300px;
grid-template-rows: auto 1fr auto;
min-height: 100vh;
gap: theme(spacing.4);
}
.dashboard-header { grid-area: header; }
.dashboard-sidebar { grid-area: sidebar; }
.dashboard-main { grid-area: main; }
.dashboard-aside { grid-area: aside; }
.dashboard-footer { grid-area: footer; }
@media (max-width: theme(breakpoint.lg)) {
.dashboard-layout {
grid-template-areas: 'header' 'main' 'aside' 'footer';
grid-template-columns: 1fr;
}
.dashboard-sidebar { display: none; }
}
}Subgrid for Card Alignment
@layer components {
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: theme(spacing.6);
}
.card {
display: grid;
grid-template-rows: subgrid;
grid-row: span 3;
}
}Animations
Keyframe Animations via @theme
@theme {
--animate-fade-in: fade-in 0.5s ease-out;
--animate-slide-up: slide-up 0.3s ease-out;
--animate-bounce-in: bounce-in 0.6s cubic-bezier(0.68, -0.55, 0.265, 1.55);
}
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slide-up {
from { transform: translateY(20px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
@keyframes bounce-in {
0% { transform: scale(0.3); opacity: 0; }
50% { transform: scale(1.05); }
70% { transform: scale(0.9); }
100% { transform: scale(1); opacity: 1; }
}<div class="animate-fade-in">Fades in</div>
<div class="animate-slide-up">Slides up</div>Scroll-Driven Animations
@layer utilities {
.animate-on-scroll {
animation: fade-in linear;
animation-timeline: view();
animation-range: entry 0% cover 30%;
}
}Reduced Motion
Always respect user preferences:
@media (prefers-reduced-motion: reduce) {
.animate-fade-in { animation: none; opacity: 1; }
.transition-all { transition: none; }
}Custom Utilities
@utility Directive
@utility text-balance {
text-wrap: balance;
}
@utility scroll-snap-x {
scroll-snap-type: x mandatory;
}
@utility scroll-snap-child {
scroll-snap-align: start;
scroll-snap-stop: always;
}Cascade Layers
@import "tailwindcss";
@layer base {
html { font-family: theme(fontFamily.body); }
}
@layer components {
.card {
background-color: theme(colors.card);
border: 1px solid theme(colors.border);
}
}
@layer utilities {
.text-balance { text-wrap: balance; }
}Plugin Development
// plugins/custom-utilities.js
export default function customUtilities({ addUtilities }) {
addUtilities({
'.text-shadow': { 'text-shadow': '0 2px 4px rgba(0,0,0,0.1)' },
'.text-shadow-lg': { 'text-shadow': '0 4px 8px rgba(0,0,0,0.15)' },
'.glass': {
'background': 'rgba(255, 255, 255, 0.1)',
'backdrop-filter': 'blur(10px)',
'border': '1px solid rgba(255, 255, 255, 0.2)',
},
});
}// vite.config.js
import tailwindcss from '@tailwindcss/vite'
import customUtilities from './plugins/custom-utilities'
export default {
plugins: [tailwindcss({ plugins: [customUtilities] })],
}Performance
Vite Plugin (100x+ faster incremental builds)
import tailwindcss from '@tailwindcss/vite'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [tailwindcss()],
build: { cssMinify: 'lightningcss' },
})Content Detection Optimization (monorepos)
@import "tailwindcss" layer(base, components, utilities)
source("./src/**/*.{js,jsx,ts,tsx}");Bundle Size Analysis
import { visualizer } from 'rollup-plugin-visualizer'
export default defineConfig({
plugins: [
tailwindcss(),
visualizer({ open: true, gzipSize: true, brotliSize: true }),
],
})Design Token Architecture
Three-Layer System
A scalable design system uses three layers: Primitive → Semantic → Component.
Layer 1: Primitives (Base Palette)
Raw color values — never use directly in components.
@theme {
--primitive-purple-400: oklch(0.60 0.25 270);
--primitive-purple-500: oklch(0.55 0.25 270);
--primitive-purple-600: oklch(0.50 0.25 270);
--primitive-blue-400: oklch(0.65 0.20 250);
--primitive-blue-500: oklch(0.60 0.20 250);
--primitive-blue-600: oklch(0.55 0.20 250);
--primitive-green-500: oklch(0.65 0.18 145);
--primitive-green-600: oklch(0.60 0.18 145);
--primitive-red-500: oklch(0.60 0.22 25);
--primitive-red-600: oklch(0.55 0.22 25);
/* Neutral scale */
--primitive-gray-50: oklch(0.98 0.01 250);
--primitive-gray-100: oklch(0.96 0.01 250);
--primitive-gray-200: oklch(0.92 0.01 250);
--primitive-gray-300: oklch(0.85 0.01 250);
--primitive-gray-400: oklch(0.70 0.01 250);
--primitive-gray-500: oklch(0.50 0.01 250);
--primitive-gray-600: oklch(0.40 0.01 250);
--primitive-gray-700: oklch(0.30 0.01 250);
--primitive-gray-800: oklch(0.20 0.01 250);
--primitive-gray-900: oklch(0.15 0.01 250);
}Layer 2: Semantic Tokens
Map primitives to meaning. These are what components should reference.
@theme {
--color-brand-primary: var(--primitive-purple-500);
--color-brand-secondary: var(--primitive-blue-500);
--color-success: var(--primitive-green-500);
--color-error: var(--primitive-red-500);
--color-warning: oklch(0.75 0.18 85);
--color-info: var(--primitive-blue-500);
--color-background: var(--primitive-gray-50);
--color-foreground: var(--primitive-gray-900);
--color-surface: oklch(1.0 0 0);
--color-border: var(--primitive-gray-300);
}Layer 3: Component Tokens
Bind semantics to specific component states.
@theme {
/* Button */
--color-button-primary-bg: var(--color-brand-primary);
--color-button-primary-hover: var(--primitive-purple-600);
--color-button-primary-text: oklch(1.0 0 0);
--color-button-primary-disabled-bg: var(--primitive-gray-300);
--color-button-primary-disabled-text: var(--primitive-gray-500);
--color-button-destructive-bg: var(--color-error);
--color-button-destructive-hover: var(--primitive-red-600);
--color-button-destructive-text: oklch(1.0 0 0);
--color-button-outline-bg: transparent;
--color-button-outline-hover: var(--primitive-gray-100);
--color-button-outline-border: var(--color-border);
--color-button-outline-text: var(--color-foreground);
/* Input */
--color-input-bg: var(--color-surface);
--color-input-border: var(--color-border);
--color-input-border-focus: var(--color-brand-primary);
--color-input-border-error: var(--color-error);
--color-input-placeholder: var(--primitive-gray-500);
/* Card */
--color-card-bg: var(--color-surface);
--color-card-border: var(--color-border);
--color-card-hover-border: var(--primitive-gray-400);
}Usage in Components
<button class="
bg-button-primary-bg text-button-primary-text
hover:bg-button-primary-hover
disabled:bg-button-primary-disabled-bg disabled:text-button-primary-disabled-text
px-4 py-2 rounded-md
">
Submit
</button>
<input class="
bg-input-bg border border-input-border
focus:border-input-border-focus focus:ring-2 focus:ring-primary
px-3 py-2 rounded-md
" />Status Color Patterns
@theme {
--status-success-bg: oklch(0.95 0.03 145);
--status-success-border: oklch(0.80 0.10 145);
--status-success-text: var(--primitive-green-600);
--status-warning-bg: oklch(0.95 0.03 85);
--status-warning-border: oklch(0.85 0.10 85);
--status-warning-text: oklch(0.45 0.18 85);
--status-error-bg: oklch(0.95 0.03 25);
--status-error-border: oklch(0.80 0.10 25);
--status-error-text: var(--primitive-red-600);
--status-info-bg: oklch(0.95 0.03 250);
--status-info-border: oklch(0.80 0.10 250);
--status-info-text: var(--primitive-blue-600);
}<div class="bg-status-success-bg border border-status-success-border text-status-success-text p-4 rounded-md">
Operation completed successfully.
</div>Color Manipulation with color-mix
Generate variations from a single base color:
@theme {
--color-primary: oklch(0.55 0.25 270);
--color-primary-light: color-mix(in oklch, var(--color-primary), white 20%);
--color-primary-dark: color-mix(in oklch, var(--color-primary), black 20%);
--color-primary-muted: color-mix(in oklch, var(--color-primary), var(--color-background) 70%);
}Accessibility Tokens
Ensure WCAG AA compliance (4.5:1 for normal text, 3:1 for large text):
@theme {
--ring-color: var(--color-primary);
--ring-offset-color: var(--color-background);
--ring-offset-width: 2px;
--ring-width: 2px;
}<button class="focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:ring-offset-background">
Accessible button
</button>Migrating from Tailwind v3 to v4
Key Changes
1. Configuration: tailwind.config.js → @theme in CSS 2. Import: @tailwind base/components/utilities → @import "tailwindcss" 3. Colors: Default palette now uses OKLCH 4. Utilities: Many class names renamed for consistency 5. Browser support: Requires Safari 16.4+, Chrome 111+, Firefox 128+
Migration Steps
1. Run the Automated Upgrade
npx @tailwindcss/upgradeThis handles most class name renames and import updates automatically.
2. Convert Config to @theme
Move your tailwind.config.js color/spacing/font definitions into CSS:
/* Before: tailwind.config.js */
/* colors: { primary: '#7c3aed' } */
/* After: app.css */
@import "tailwindcss";
@theme {
--color-primary: oklch(0.55 0.25 270);
}3. Update CSS Imports
/* Before */
@tailwind base;
@tailwind components;
@tailwind utilities;
/* After */
@import "tailwindcss";4. Common Class Name Renames
v3 → v4
───────────────────────────────
bg-gradient-to-r → bg-linear-to-r
bg-gradient-to-br → bg-linear-to-br
flex-shrink-0 → shrink-0
flex-grow → grow
decoration-clone → box-decoration-clone
decoration-slice → box-decoration-slice5. Common Breaking Changes
| v3 Pattern | Issue in v4 | Fix |
|---|---|---|
tailwind.config.js plugins | Plugin API changed | Update to v4 plugin format or use @utility |
@apply in components | Discouraged | Use theme() or utility classes directly |
purge / content config | Removed | v4 auto-detects; use source() for monorepos |
darkMode: 'class' | Not a JS option | Use @media (prefers-color-scheme: dark) or [data-theme] |
| Custom color palette in JS | Not in JS anymore | Define in @theme with OKLCH values |
6. Testing Checklist
After migration, verify:
- [ ] Color rendering matches design (OKLCH may shift some colors)
- [ ] Custom utilities still work
- [ ] Dark mode toggles correctly
- [ ] Plugin functionality intact
- [ ] No dynamic class construction broken by renames
- [ ] Build output size is similar or smaller
Theming
Dark Mode with @theme
/* Base theme (light) */
@theme {
--color-background: oklch(1.0 0 0);
--color-foreground: oklch(0.20 0.01 250);
--color-surface: oklch(0.98 0.01 250);
--color-card: oklch(1.0 0 0);
--color-border: oklch(0.90 0.01 250);
--color-text-primary: oklch(0.20 0.01 250);
--color-text-secondary: oklch(0.45 0.01 250);
--color-primary: oklch(0.55 0.25 270);
}
/* Dark theme — automatic via system preference */
@media (prefers-color-scheme: dark) {
@theme {
--color-background: oklch(0.15 0.01 250);
--color-foreground: oklch(0.95 0.01 250);
--color-surface: oklch(0.20 0.01 250);
--color-card: oklch(0.18 0.01 250);
--color-border: oklch(0.30 0.01 250);
--color-text-primary: oklch(0.95 0.01 250);
--color-text-secondary: oklch(0.65 0.01 250);
--color-primary: oklch(0.65 0.25 270); /* Lighter in dark mode */
}
}Manual Theme Override with data-theme
[data-theme="light"] {
@theme {
--color-background: oklch(1.0 0 0);
--color-foreground: oklch(0.20 0.01 250);
}
}
[data-theme="dark"] {
@theme {
--color-background: oklch(0.15 0.01 250);
--color-foreground: oklch(0.95 0.01 250);
}
}Brand Theme Variants
[data-theme="brand-purple"] {
@theme {
--color-primary: oklch(0.55 0.25 270);
--color-secondary: oklch(0.60 0.20 250);
}
}
[data-theme="brand-green"] {
@theme {
--color-primary: oklch(0.65 0.18 145);
--color-secondary: oklch(0.70 0.20 85);
}
}Theme Switching (JavaScript)
type Theme = 'light' | 'dark' | 'system';
function setTheme(theme: Theme) {
const root = document.documentElement;
if (theme === 'system') {
root.removeAttribute('data-theme');
} else {
root.setAttribute('data-theme', theme);
}
localStorage.setItem('theme', theme);
}
function getTheme(): Theme {
return (localStorage.getItem('theme') as Theme) ?? 'system';
}
// Initialize on page load
document.addEventListener('DOMContentLoaded', () => {
setTheme(getTheme());
});Runtime Dynamic Theming
For apps that allow users to pick a brand color at runtime:
@theme {
--color-primary: var(--runtime-primary, oklch(0.55 0.25 270));
--color-secondary: var(--runtime-secondary, oklch(0.65 0.20 340));
--color-background: var(--runtime-background, oklch(1.0 0 0));
--color-foreground: var(--runtime-foreground, oklch(0.20 0.01 250));
}interface ThemeColors {
primary: string;
secondary: string;
background: string;
foreground: string;
}
class ThemeManager {
private root = document.documentElement;
setTheme(colors: Partial<ThemeColors>) {
Object.entries(colors).forEach(([key, value]) => {
if (value) this.root.style.setProperty(`--runtime-${key}`, value);
});
}
resetTheme() {
['primary', 'secondary', 'background', 'foreground'].forEach(key => {
this.root.style.removeProperty(`--runtime-${key}`);
});
}
}
export const themeManager = new ThemeManager();User Preference Detection
interface UserPreferences {
colorScheme: 'light' | 'dark' | 'auto';
reducedMotion: boolean;
highContrast: boolean;
}
function detectPreferences(): UserPreferences {
return {
colorScheme: (localStorage.getItem('color-scheme') as 'light' | 'dark') ?? 'auto',
reducedMotion: window.matchMedia('(prefers-reduced-motion: reduce)').matches,
highContrast: window.matchMedia('(prefers-contrast: high)').matches,
};
}
function applyPreferences(prefs: UserPreferences) {
const root = document.documentElement;
if (prefs.colorScheme === 'auto') {
root.removeAttribute('data-theme');
} else {
root.setAttribute('data-theme', prefs.colorScheme);
}
root.classList.toggle('reduce-motion', prefs.reducedMotion);
root.classList.toggle('high-contrast', prefs.highContrast);
}Responsive Tokens
Mobile-first token overrides:
@theme {
--spacing-section: theme(spacing.8);
--spacing-card: theme(spacing.4);
@media (min-width: theme(breakpoint.md)) {
--spacing-section: theme(spacing.12);
--spacing-card: theme(spacing.6);
}
@media (min-width: theme(breakpoint.lg)) {
--spacing-section: theme(spacing.16);
--spacing-card: theme(spacing.8);
}
}