Build, style, and maintain modern responsive web interfaces using Tailwind CSS across versions 3 and 4. Use when designing responsive layouts, configuring design tokens, implementing dark mode, structuring reusable components with clsx and tailwind-merge, creating animations, or managing v3 and v4 configurations in frontend applications.
64
80%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Passed
No findings from the security scan
Fix and improve this skill with Tessl
tessl review fix ./skills/development-skills/tailwind-css-development/SKILL.mdTailwind CSS is a utility-first CSS framework that composes atomic utility classes directly within markup. Rather than writing ad-hoc CSS stylesheets and inventing arbitrary class names, developers assemble UIs using structured design constraints for spacing, color, typography, elevation, and layout. This eliminates stylesheet bloat, prevents CSS regression cascades across features, and dramatically speeds up UI iteration.
For detailed UI component recipes, responsive layout templates, and syntax migration tables, consult utility-patterns.md.
sm, md, lg, xl, 2xl) or container queries (@container).tailwind.config.ts) or v4 (@theme).cn(), clsx, tailwind-merge).@apply CSS abstractions.npm, pnpm, yarn, or bun.tailwindcss (v4.x) and @tailwindcss/vite (or @tailwindcss/postcss).tailwindcss (v3.x), postcss, and autoprefixer.clsx and tailwind-merge for robust conditional class composition.Determine whether the project is on Tailwind CSS v3 (JavaScript-first) or v4 (CSS-first Oxide engine):
package.json dependencies for tailwindcss:
^3.x.x: Uses tailwind.config.js or tailwind.config.ts, PostCSS, and @tailwind directives.^4.x.x: Uses CSS-first configuration (@import "tailwindcss";), @theme, and build tool plugins like @tailwindcss/vite.src/index.css, app/globals.css, or styles/main.css):
@tailwind base;
@tailwind components;
@tailwind utilities;@import "tailwindcss";To install Tailwind in a new project:
# For Tailwind CSS v4 (Modern Default)
npm install tailwindcss @tailwindcss/postcss postcss
# Or in a Vite project:
npm install tailwindcss @tailwindcss/vite
# For Tailwind CSS v3 (Legacy/LTS)
npm install -D tailwindcss@^3 postcss autoprefixer
npx tailwindcss init -p # Generates tailwind.config.js and postcss.config.jsDesign tokens define the constraint system for the application. Always extend rather than overwrite default tokens unless an intentional reset is required.
tailwind.config.ts)import type { Config } from 'tailwindcss';
const config: Config = {
content: [
'./pages/**/*.{js,ts,jsx,tsx,mdx}',
'./components/**/*.{js,ts,jsx,tsx,mdx}',
'./app/**/*.{js,ts,jsx,tsx,mdx}',
'./src/**/*.{js,ts,jsx,tsx,mdx}',
],
darkMode: 'class', // 'media' for system default, 'class' for manual toggle
theme: {
extend: {
colors: {
brand: {
50: '#f0fdf4',
500: '#22c55e',
900: '#14532d',
},
surface: {
light: '#ffffff',
dark: '#0f172a',
},
},
fontFamily: {
sans: ['var(--font-sans)', 'system-ui', 'sans-serif'],
display: ['var(--font-heading)', 'serif'],
},
spacing: {
'128': '32rem',
'144': '36rem',
},
borderRadius: {
'4xl': '2rem',
},
},
},
plugins: [],
};
export default config;@theme)In Tailwind v4, configure tokens directly inside your CSS file without a separate JS file:
@import "tailwindcss";
@theme {
/* Colors (Modern OKLCH color space supported) */
--color-brand-50: oklch(0.97 0.05 140);
--color-brand-500: oklch(0.70 0.20 140);
--color-brand-900: oklch(0.35 0.15 140);
/* Fonts */
--font-sans: var(--font-sans), system-ui, sans-serif;
--font-display: var(--font-heading), serif;
/* Custom Spacing */
--spacing-128: 32rem;
/* Breakpoints */
--breakpoint-3xl: 1920px;
}When a design requires a one-off value not in the design system, use bracket syntax:
<!-- Arbitrary values -->
<div class="top-[117px] bg-[#1da1f2] p-[clamp(1rem,5vw,3rem)]">
<!-- Arbitrary properties -->
<div class="[mask-type:alpha] [clip-path:polygon(0_0,100%_0,100%_75%,0_100%)]">
Arbitrary CSS
</div>
</div>Tailwind is mobile-first by default. Unprefixed utilities apply to all screen widths, while breakpoint prefixes apply at that breakpoint and above.
| Breakpoint | Minimum Width | CSS Media Query |
|---|---|---|
sm | 640px | @media (min-width: 640px) { ... } |
md | 768px | @media (min-width: 768px) { ... } |
lg | 1024px | @media (min-width: 1024px) { ... } |
xl | 1280px | @media (min-width: 1280px) { ... } |
2xl | 1536px | @media (min-width: 1536px) { ... } |
<div class="w-full md:w-1/2 lg:w-1/3"> (Mobile full, tablet half, desktop third).<div class="w-1/3 max-md:w-full"> (Avoid negative/max modifiers unless strictly necessary).<!-- Mobile-first responsive card layout -->
<div class="flex flex-col gap-4 p-4 sm:flex-row sm:items-center sm:justify-between sm:p-6 lg:p-8">
<div>
<h2 class="text-lg font-bold sm:text-xl lg:text-2xl">Project Velocity</h2>
<p class="text-sm text-slate-500 sm:text-base">Track delivery metrics over time.</p>
</div>
<button class="w-full rounded-lg bg-indigo-600 px-4 py-2 text-white sm:w-auto">
Export Data
</button>
</div>Style elements based on the width of their parent container rather than the browser viewport:
@tailwindcss/container-queries. In Tailwind v4, container queries are built-in.@container.@sm:, @md:, @lg:, @xl:, etc.<!-- Parent marked as a query container -->
<div class="@container w-full max-w-2xl rounded-xl border border-slate-200 p-4">
<div class="flex flex-col gap-4 @md:flex-row @md:items-center">
<div class="h-20 w-20 rounded-lg bg-indigo-500 @md:h-28 @md:w-28"></div>
<div class="flex-1">
<h3 class="text-base font-bold @md:text-xl">Container Adaptive Card</h3>
<p class="text-sm text-slate-600 @md:text-base">Refits layout when parent resizes.</p>
</div>
</div>
</div>Choose between media (automatically matches user OS system preference) or class / selector (allows in-app toggling via theme switchers).
class strategy - Recommended for apps)// tailwind.config.ts
darkMode: 'class', // or ['selector', '[data-theme="dark"]']/* In v4, class strategy can be configured with a custom variant */
@custom-variant dark (&:where(.dark, .dark *));dark: prefix:
<div class="bg-white text-slate-900 dark:bg-slate-900 dark:text-slate-100">
<h1 class="text-slate-800 dark:text-white">Theme-aware Content</h1>
<p class="text-slate-600 dark:text-slate-400">Works seamlessly in light and dark mode.</p>
</div>Instead of littering markup with duplicate dark: utilities on every element, use CSS variables for semantic surfaces:
:root {
--bg-page: #ffffff;
--bg-card: #f8fafc;
--text-main: #0f172a;
--border-subtle: #e2e8f0;
}
.dark {
--bg-page: #0b0f19;
--bg-card: #111827;
--text-main: #f8fafc;
--border-subtle: #1f2937;
}Map variables to Tailwind tokens:
/* Tailwind v4 */
@theme {
--color-page: var(--bg-page);
--color-card: var(--bg-card);
--color-main: var(--text-main);
--color-subtle: var(--border-subtle);
}Now <div class="bg-card text-main border-subtle"> automatically switches palettes with zero modifier overhead.
@apply| Scenario | Best Approach | Why |
|---|---|---|
| Framework components (React, Vue, Svelte, Astro) | Component Extraction | Preserves utility visibility, keeps bundle minimal, leverages TypeScript types |
| Dynamic state, variant props, size modifiers | clsx + tailwind-merge | Handles conflicting utility overrides cleanly without CSS specificity wars |
| Multi-element third-party HTML (Markdown/CMS) | @tailwindcss/typography (prose) | Avoids manually tagging injected HTML |
Highly repetitive global base elements (input, button resets) | @layer base or @utility | Single point of normalization |
Anti-pattern: Creating arbitrary CSS classes with dozens of @applys | Avoid | Defeats utility-first benefits, increases CSS bundle size, breaks purge detection |
cn() Helper ImplementationAlways combine clsx (conditional joins) and tailwind-merge (conflict resolution):
npm install clsx tailwind-mergeCreate lib/utils.ts:
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]): string {
return twMerge(clsx(inputs));
}Example: Production-grade Button component with variants, sizes, and prop merging:
import * as React from 'react';
import { cn } from '@/lib/utils';
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger';
size?: 'sm' | 'md' | 'lg';
isLoading?: boolean;
}
const variantStyles: Record<NonNullable<ButtonProps['variant']>, string> = {
primary: 'bg-indigo-600 text-white hover:bg-indigo-500 active:bg-indigo-700 shadow-sm',
secondary: 'bg-slate-100 text-slate-900 hover:bg-slate-200 dark:bg-slate-800 dark:text-slate-100 dark:hover:bg-slate-700',
outline: 'border border-slate-300 bg-transparent text-slate-700 hover:bg-slate-50 dark:border-slate-700 dark:text-slate-200 dark:hover:bg-slate-800',
ghost: 'text-slate-700 hover:bg-slate-100 dark:text-slate-200 dark:hover:bg-slate-800',
danger: 'bg-rose-600 text-white hover:bg-rose-500 active:bg-rose-700 shadow-sm',
};
const sizeStyles: Record<NonNullable<ButtonProps['size']>, string> = {
sm: 'h-8 px-3 text-xs gap-1.5',
md: 'h-10 px-4 text-sm gap-2',
lg: 'h-12 px-6 text-base gap-2.5',
};
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant = 'primary', size = 'md', isLoading = false, disabled, children, ...props }, ref) => {
return (
<button
ref={ref}
disabled={disabled || isLoading}
className={cn(
// Base styles
'inline-flex items-center justify-center font-medium rounded-lg transition-colors',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2',
'disabled:pointer-events-none disabled:opacity-50 select-none',
// Variants
variantStyles[variant],
sizeStyles[size],
// User overrides
className
)}
{...props}
>
{isLoading && (
<svg className="h-4 w-4 animate-spin text-current" viewBox="0 0 24 24" fill="none">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
</svg>
)}
{children}
</button>
);
}
);
Button.displayName = 'Button';Why cn() is essential: If a caller writes <Button className="bg-red-500 text-black">, twMerge detects that bg-red-500 conflicts with bg-indigo-600 and removes the latter, avoiding unpredictable CSS cascade ordering bugs.
Tailwind provides transitions for smooth property changes and CSS animations for continuous or triggered motions.
<!-- Interactive Card using group modifier -->
<div class="group relative rounded-xl border border-slate-200 p-6 transition-all duration-200 hover:-translate-y-1 hover:shadow-lg hover:border-indigo-300">
<h3 class="text-lg font-semibold text-slate-800 transition-colors group-hover:text-indigo-600">
Interactive Feature
</h3>
<span class="text-sm text-slate-500 transition-transform duration-200 group-hover:translate-x-1 inline-block">
Explore feature →
</span>
</div><label class="flex items-center gap-3 cursor-pointer">
<input type="checkbox" class="peer sr-only" />
<div class="h-6 w-11 rounded-full bg-slate-300 peer-checked:bg-indigo-600 peer-focus-visible:ring-2 peer-focus-visible:ring-indigo-500 transition-colors"></div>
<span class="text-sm text-slate-700 peer-checked:text-indigo-900 font-medium select-none">
Sync automatically
</span>
</label>In Tailwind v3 (tailwind.config.ts):
theme: {
extend: {
keyframes: {
shimmer: {
'100%': { transform: 'translateX(100%)' },
},
},
animation: {
shimmer: 'shimmer 1.5s infinite',
},
},
}In Tailwind v4 (CSS):
@theme {
--animate-shimmer: shimmer 1.5s infinite;
}
@keyframes shimmer {
100% {
transform: translateX(100%);
}
}@tailwindcss/typography): Adds the prose class for rendering markdown and rich-text.@tailwindcss/forms): Resets form element styles for easy styling.@tailwindcss/container-queries): Viewport-independent responsive layouts (built into v4).Tailwind v3 Plugin Definition:
import plugin from 'tailwindcss/plugin';
export default {
plugins: [
plugin(function ({ addUtilities }) {
addUtilities({
'.text-balance': {
'text-wrap': 'balance',
},
});
}),
],
};Tailwind v4 Native CSS Directives:
/* Declare custom utility in CSS */
@utility text-balance {
text-wrap: balance;
}
/* Declare custom variant */
@custom-variant pointer-coarse (@media (pointer: coarse));prettier-plugin-tailwindcss to automatically sort utility classes (box model $\rightarrow$ positioning $\rightarrow$ flex/grid $\rightarrow$ typography $\rightarrow$ visual $\rightarrow$ modifiers).className={text-${color}-500} (Tailwind's scanner cannot detect interpolated strings at build time).const colorMap = {
blue: 'text-blue-500',
green: 'text-green-500',
red: 'text-red-500',
};
<span className={colorMap[color]} />focus-visible Instead of focus: Ensures focus rings only appear for keyboard navigation and accessibility, not mouse clicks.@apply Sprawl: Abstract repetitive UI patterns into reusable components in your UI framework rather than dumping hundreds of utility declarations into custom CSS classes.content array in tailwind.config.ts.content. In v4, the Oxide scanner resolves this automatically.`${baseClasses} ${className}`) fails because CSS rule order determines specificity, not class string order.cn() (twMerge(clsx(...))).theme: { colors: { ... } } replaces the entire default palette.theme: { extend: { colors: { ... } } }.<head> that reads localStorage and appends .dark to document.documentElement before render.Confirm proper Tailwind installation, purging, and styling application:
Verify Development Server Compilation:
# Run the framework dev command (e.g., Vite, Next.js)
npm run devEnsure utilities render without PostCSS or build parsing errors.
Verify Production Build and Purging:
npm run buildCheck the output .css file in the build artifacts (e.g., dist/ or .next/static/css/). The production CSS file should be small (typically under 20-40 KB gzipped) and should contain only utilities actually used in the codebase.
Verify Interactive & Responsive States:
.dark on <html> or <body> to verify dark mode styles activate cleanly.Tab) to ensure focus-visible:ring-2 indicators are prominent and accessible.87555db
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.