| name | tailwind-ops |
| description | Tailwind CSS utility patterns, responsive design, component patterns, v4 migration, and configuration. Use for: tailwind, tailwindcss, utility classes, responsive design, dark mode, tailwind v4, tailwind config, tw, container queries, @apply, prose, typography, animation. |
| license | MIT |
| allowed-tools | Read Write Bash |
| metadata | {"author":"claude-mods","related-skills":"react-ops, vue-ops, astro-ops"} |
Tailwind Operations
Comprehensive Tailwind CSS patterns covering layout, responsive design, components, dark mode, animations, and v4 migration.
Tailwind v4 ecosystem facts verified as of 2026-07.
Layout Decision Tree
Which layout approach?
│
├─ Items in a single row or column?
│ └─ Use Flexbox
│ ├─ Row: class="flex items-center gap-4"
│ ├─ Column: class="flex flex-col gap-4"
│ ├─ Wrap: class="flex flex-wrap gap-4"
│ └─ Push item to end: class="flex" + child class="ml-auto"
│
├─ Items in a 2D grid (rows AND columns)?
│ └─ Use CSS Grid
│ ├─ Equal columns: class="grid grid-cols-3 gap-6"
│ ├─ Responsive grid: class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"
│ ├─ Sidebar layout: class="grid grid-cols-[250px_1fr] gap-6"
│ ├─ Spanning: child class="col-span-2" or "row-span-2"
│ └─ Auto-fill: class="grid grid-cols-[repeat(auto-fill,minmax(250px,1fr))] gap-6"
│
├─ Component should adapt to its CONTAINER size (not viewport)?
│ └─ Use Container Queries (v3.2+ / v4 native)
│ ├─ Parent: class="@container"
│ ├─ Child: class="@sm:flex-row @lg:grid-cols-3"
│ └─ Named: class="@container/sidebar" → child: "@sm/sidebar:flex-row"
│
├─ Centering something?
│ ├─ Horizontal text: class="text-center"
│ ├─ Horizontal block: class="mx-auto" (needs width)
│ ├─ Flex center: class="flex items-center justify-center"
│ ├─ Grid center: class="grid place-items-center"
│ └─ Absolute center: class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2"
│
└─ Full-page layout (header/sidebar/content/footer)?
└─ Use Grid with named areas or template rows
├─ Sticky header: class="grid grid-rows-[auto_1fr_auto] min-h-screen"
└─ Sidebar + main: class="grid grid-cols-[250px_1fr] min-h-screen"
Responsive Design Quick Reference
Breakpoints (Mobile-First)
| Prefix | Min Width | Typical Target |
|---|
| (none) | 0px | Mobile (default) |
sm: | 640px | Large phones, landscape |
md: | 768px | Tablets |
lg: | 1024px | Small laptops |
xl: | 1280px | Desktops |
2xl: | 1536px | Large screens |
Mobile-first means: base styles apply to mobile, add breakpoint prefixes to override upward.
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div>Card 1</div>
<div>Card 2</div>
<div>Card 3</div>
</div>
<nav class="hidden lg:flex items-center gap-6">...</nav>
<div class="w-full max-w-3xl mx-auto px-4 sm:px-6 lg:px-8">...</div>
Container Queries
<div class="@container">
<div class="flex flex-col @sm:flex-row @lg:grid @lg:grid-cols-3 gap-4">
<div>Adapts to container</div>
</div>
</div>
<div class="@container/card">
<h2 class="text-sm @md/card:text-lg">Responds to card container</h2>
</div>
Fluid Typography with clamp()
<h1 class="text-[clamp(1.5rem,4vw,3rem)]">Fluid Heading</h1>
<p class="text-[clamp(0.875rem,1.5vw,1.125rem)] leading-relaxed">
Body text that scales smoothly.
</p>
Dark Mode Decision Tree
Which dark mode strategy?
│
├─ Manual toggle (user preference stored)?
│ └─ class strategy (v3) / selector strategy (v4)
│
│ v3: tailwind.config.js
│ module.exports = { darkMode: 'class' }
│ → Add class="dark" to <html> element
│
│ v4: CSS @custom-variant or default behavior
│ @custom-variant dark (&:where(.dark, .dark *));
│ → Same toggle, add class="dark" to <html>
│
├─ Follow system preference only?
│ └─ media strategy
│
│ v3: tailwind.config.js
│ module.exports = { darkMode: 'media' }
│ → Uses prefers-color-scheme automatically
│
│ v4: Default behavior (no config needed)
│ → Uses prefers-color-scheme out of the box
│
└─ Custom selector (data attribute, etc.)?
└─ selector strategy (v4 only)
v4: @custom-variant dark (&:where([data-theme="dark"], [data-theme="dark"] *));
→ Add data-theme="dark" to <html>
Dark Mode Patterns
<div class="bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100">
<div class="bg-gray-50 dark:bg-gray-800 rounded-lg p-6 border border-gray-200 dark:border-gray-700">
<h3 class="text-gray-900 dark:text-white font-semibold">Card Title</h3>
<p class="text-gray-600 dark:text-gray-400">Card content adapts to dark mode.</p>
</div>
<input type="text"
class="bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600
text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500
focus:ring-2 focus:ring-blue-500 rounded-lg px-4 py-2"
placeholder="Type here...">
</div>
Component Patterns Quick Reference
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">Title</h3>
<p class="text-gray-600 dark:text-gray-400">Content here.</p>
</div>
<button class="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 transition-colors">Primary</button>
<button class="bg-gray-200 text-gray-800 px-4 py-2 rounded-lg hover:bg-gray-300 transition-colors">Secondary</button>
<button class="border border-gray-300 text-gray-700 px-4 py-2 rounded-lg hover:bg-gray-50 transition-colors">Outline</button>
<button class="text-blue-600 px-4 py-2 rounded-lg hover:bg-blue-50 transition-colors">Ghost</button>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Email</label>
< =
=
=>
Logo
Home
CTA
Modal Title
Modal content goes here.
Cancel
Confirm
Active
Error
Something went wrong. Please try again.
Tailwind v4 Quick Reference
Major Changes from v3
| Area | v3 | v4 |
|---|
| Configuration | tailwind.config.js | CSS-first: @theme in CSS |
| Theme values | JS theme.extend.colors | @theme { --color-brand: #3b82f6; } |
| Plugins | JS plugin() function | @plugin "my-plugin" in CSS |
| Config file | module.exports = {...} | @config "./legacy.config.js" (compat) |
| PostCSS | tailwindcss package | @tailwindcss/postcss |
| Vite | PostCSS plugin | @tailwindcss/vite (faster) |
| Colors | Named scales (gray-50..950) | Same + OKLCH support |
| Container queries | Plugin required | Native @container, @sm:, @md: |
| Entry animations | JS needed | @starting-style (CSS native) |
v4 CSS-First Config
@import "tailwindcss";
@theme {
--color-brand: #3b82f6;
--color-brand-dark: #1d4ed8;
--font-display: "Inter", sans-serif;
--breakpoint-3xl: 1920px;
--spacing-18: 4.5rem;
}
@plugin "@tailwindcss/typography";
@config "./tailwind.config.js";
v4 New Utilities
<div class="@container">
<div class="@sm:flex @md:grid @md:grid-cols-2">Adapts to container</div>
</div>
<div class="shadow-sm ring ring-blue-500/20">Improved defaults</div>
Animation Patterns
Transition Utilities
<button class="bg-blue-600 hover:bg-blue-700 transition-colors duration-150">
Hover me
</button>
<div class="transform hover:scale-105 hover:shadow-lg transition-all duration-200 ease-in-out">
Scale and shadow on hover
</div>
<div class="transition-[transform,opacity] duration-300 ease-out">
Only transform and opacity animate
</div>
Built-in Animations
<svg class="animate-spin h-5 w-5 text-blue-600" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none"/>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/>
</svg>
<div class="animate-pulse bg-gray-200 dark:bg-gray-700 h-4 rounded w-3/4"></div>
<span class="relative flex h-3 w-3">
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-red-400 opacity-75"></span>
<span class="relative inline-flex rounded-full h-3 w-3 bg-red-500"></span>
Custom Keyframes (v3 Config)
module.exports = {
theme: {
extend: {
keyframes: {
'fade-in': {
'0%': { opacity: '0', transform: 'translateY(10px)' },
'100%': { opacity: '1', transform: 'translateY(0)' },
},
'slide-in-right': {
'0%': { transform: 'translateX(100%)' },
'100%': { transform: 'translateX(0)' },
},
},
animation: {
'fade-in': 'fade-in 0.3s ease-out',
'slide-in-right': 'slide-in-right 0.3s ease-out',
},
},
},
}
Custom Keyframes (v4 CSS)
@theme {
--animate-fade-in: fade-in 0.3s ease-out;
--animate-slide-in-right: slide-in-right 0.3s ease-out;
}
@keyframes fade-in {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes slide-in-right {
from { transform: translateX(100%); }
to { transform: translateX(0); }
}
Entry Animations with @starting-style (v4)
dialog[open] {
opacity: 1;
transform: translateY(0);
transition: opacity 0.3s, transform 0.3s;
@starting-style {
opacity: 0;
transform: translateY(10px);
}
}
State Modifiers Quick Reference
| Modifier | Triggers On | Example |
|---|
hover: | Mouse hover | hover:bg-blue-700 |
focus: | Element focused (all focus) | focus:ring-2 |
focus-visible: | Keyboard focus only | focus-visible:outline-2 |
focus-within: | Child is focused | focus-within:ring-2 |
active: | Being clicked/pressed | active:scale-95 |
disabled: | disabled attribute | disabled:opacity-50 disabled:cursor-not-allowed |
group-hover: | Parent .group hovered | group-hover:text-blue-600 |
group-focus: | Parent .group focused | group-focus:ring-2 |
peer-checked: | Sibling .peer checked | peer-checked:bg-blue-600 |
peer-invalid: | Sibling .peer invalid | peer-invalid:text-red-500 |
data-[state=open]: | Custom data attribute | data-[state=open]:rotate-180 |
aria-expanded: | aria-expanded="true" | aria-expanded:bg-gray-100 |
aria-selected: | aria-selected="true" | aria-selected:font-bold |
open: | <details> or <dialog> open | open:bg-gray-50 |
|
Group and Peer Patterns
<a href="#" class="group flex items-center gap-3 p-3 rounded-lg hover:bg-gray-100">
<div class="w-10 h-10 bg-gray-200 group-hover:bg-blue-100 rounded-lg"></div>
<span class="text-gray-700 group-hover:text-blue-600">Hover the whole card</span>
</a>
<div class="group/card p-4">
<div class="group/button">
<span class="group-hover/card:text-blue-600 group-hover/button:underline">
Responds to specific parent
</span>
</div>
</div>
<input type="checkbox" class="peer sr-only" id="toggle">
<label for="toggle" class="peer-checked:bg-blue-600 peer-checked:text-white px-4 py-2 rounded-lg cursor-pointer">
Toggle me
Please enter a valid email.
Common Gotchas
| Gotcha | Why | Fix |
|---|
Dynamic class names don't work: `bg-${color}-500` | Tailwind scans source for complete class strings at build time. String interpolation produces classes it never sees. | Use complete classes: const colors = { red: 'bg-red-500', blue: 'bg-blue-500' } and select by key. |
| Styles not applying (specificity) | Another CSS rule or @apply has higher specificity. | Use !important modifier: !text-red-500. Or restructure to avoid conflicts. |
@apply breaks with component libraries | @apply resolves at build time and can't access runtime theme values or conflict with scoped styles. | Prefer inline utility classes. Reserve @apply for base styles or markdown content. |
| Prose plugin styles leak | @tailwindcss/typography prose applies broad element selectors (h1, p, a, etc.). | Scope with prose only on content wrappers. Use not-prose class to exclude sections. |
| Classes missing in production | JIT content detection didn't scan the file containing the class. | Ensure content paths in config cover all template files including component libraries. |
| Dark mode flash (FOUC) | Class-based dark mode renders light first until JS adds dark class. | Add inline <script> in <head> that reads localStorage and sets dark class before paint. |
| Container queries not scoped | Child @sm: responds to nearest @container ancestor, which may not be the intended one. | Use named containers: @container/card and @sm/card:flex. |
| Arbitrary values vs config | w-[137px] works but creates one-off values. Repeated arbitrary values signal missing design tokens. | Add recurring values to theme config: spacing: { '137': '137px' }. |
group / peer naming collisions |
Reference Files
| File | Content | Lines |
|---|
references/component-patterns.md | Cards, buttons, forms, navigation, modals, tables, alerts, badges, avatars, dropdowns, tooltips, skeleton loaders, accessibility | ~700 |
references/v4-migration.md | CSS-first config, @theme, @plugin, removed utilities, container queries, @starting-style, migration steps, breaking changes | ~500 |
references/configuration.md | Theme config (v3+v4), colors, spacing, typography, plugins, @layer, @apply, custom variants, dark mode, container queries | ~500 |
Staleness Verifier
This skill encodes fast-moving facts (the Tailwind v4 CSS-first directives, the
@tailwindcss/* package set, the v3→v4 migration). scripts/check-tailwind-facts.py
guards them against silent drift — internal consistency in PR CI, live
major-version drift in the scheduled freshness job:
python3 skills/tailwind-ops/scripts/check-tailwind-facts.py --offline
python3 skills/tailwind-ops/scripts/check-tailwind-facts.py --live
The canonical fact list lives in assets/tailwind-facts.json; when you add or drop a package or the prose stops naming one, update it to match or --offline fails CI.
See Also