Tailwind CSS v4 skill for Claude Code
Tokens in @theme, complete class names, no @apply sprawl. Claude sets up Tailwind v4 the CSS-first way, puts brand colors, radii and breakpoints in @theme so utilities and CSS variables share one source, and themes through semantic variables instead of dark: on every element. It never builds class names from string fragments, extracts repetition into components with cva, and follows a checklist of renamed utilities and default changes when upgrading from v3.
- name
tailwind-css - size206 lines · ≈1,900 tokens
- files2
- licenseMIT
- updated
What Claude sees before loading it
This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.
description: Styles interfaces with Tailwind CSS v4 using CSS-first configuration — @import "tailwindcss", @theme design tokens, @utility and @custom-variant, class-based dark mode, container queries, and component extraction with cva or framework components instead of @apply sprawl. Also handles v3 to v4 upgrades and renamed utilities. Use when adding Tailwind to a project, defining brand tokens, fixing classes that do not apply, setting up dark mode, or migrating a tailwind.config.js project to v4. Try asking
- Upgrade this project from Tailwind 3 to 4 without breaking the layout
- Set up our brand palette and fonts as Tailwind tokens with a dark theme
- Some Tailwind classes work in dev but are missing in the production build
Use it when
- Adding Tailwind v4 to a Vite, Next.js or PostCSS project
- Defining design tokens, custom utilities or variants
- Migrating tailwind.config.js and v3 class names to v4
- Class lists repeat across many components
Skip it when
- The project uses plain CSS or CSS modules and needs layering or naming rules — use css-architecture
- You are designing the token system itself across platforms — use design-system-tokens
How to check the result
The production CSS contains the newly added classes, both themes pass contrast checks, and screenshots of key pages match baselines after an upgrade.
The common mistake it prevents
Composing class names like bg-${color}-500 at runtime, which the scanner cannot see, so the styles exist in dev and vanish in production.
Good Tailwind code draws every repeated value from @theme tokens, writes complete
class names the scanner can see, extracts repetition into components rather than
@apply classes, and keeps arbitrary values for genuine one-offs.
Before changing anything
npm ls tailwindcss @tailwindcss/vite @tailwindcss/postcss @tailwindcss/cli
ls tailwind.config.* postcss.config.* 2>/dev/null
grep -rn "@tailwind\|@import \"tailwindcss\"\|@theme\|@config" src --include=*.css | head
| Finding | Meaning |
|---|---|
@tailwind base; @tailwind components; @tailwind utilities; | v3 — keep v3 conventions or migrate deliberately |
@import "tailwindcss"; + @theme | v4 CSS-first |
v4 plus @config "./tailwind.config.js" | v4 running a legacy JS config — works, but new tokens belong in @theme |
For a v3 → v4 migration, read references/v3-to-v4-migration.md before running anything.
Setup (v4)
// vite.config.ts — preferred for Vite-based apps
import tailwindcss from "@tailwindcss/vite";
export default defineConfig({ plugins: [tailwindcss()] });
// postcss.config.mjs — Next.js and other PostCSS pipelines
export default { plugins: { "@tailwindcss/postcss": {} } };
/* src/app.css */
@import "tailwindcss";
@plugin "@tailwindcss/typography";
@source "../node_modules/@acme/ui/dist"; /* only for class names outside auto-detected files */
Content detection is automatic and skips .gitignored paths, so classes used inside
packages in node_modules need an explicit @source.
Tokens with @theme
@theme {
--font-sans: "Inter Variable", ui-sans-serif, system-ui, sans-serif;
--color-brand-50: oklch(0.97 0.02 265);
--color-brand-500: oklch(0.58 0.2 265);
--color-brand-700: oklch(0.45 0.18 265);
--color-surface: oklch(0.99 0 0);
--color-ink: oklch(0.21 0.02 265);
--radius-card: 0.75rem;
--breakpoint-3xl: 112rem;
--animate-fade-in: fade-in 150ms ease-out;
@keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }
}
- Each namespace generates utilities:
--color-brand-500→bg-brand-500,text-brand-500,border-brand-500;--radius-card→rounded-card;--breakpoint-3xl→3xl:. - Tokens are also real CSS variables:
var(--color-brand-500)works in plain CSS, inline styles, and canvas code. - Reset a whole default namespace to enforce a closed palette:
--color-*: initial;then define only your colors. - Use
@theme inline { --color-primary: var(--brand); }when a token references another variable, so the utility resolves the variable at use site. - Spacing is computed from
--spacing(default0.25rem):p-13,w-130work without config. Prefer the scale over arbitraryp-[52px].
Semantic tokens and dark mode
Define semantic colors once; switch their values per theme instead of sprinkling
dark: on every element.
@custom-variant dark (&:where(.dark, .dark *)); /* class-based; default is prefers-color-scheme */
:root {
--bg: var(--color-surface);
--fg: var(--color-ink);
}
.dark {
--bg: oklch(0.18 0.02 265);
--fg: oklch(0.95 0.01 265);
}
@theme inline {
--color-bg: var(--bg);
--color-fg: var(--fg);
}
Now bg-bg text-fg themes automatically. Reserve dark: for genuine exceptions
(an image that needs dark:invert). For a data-theme attribute, use
@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));.
Flash-free toggling and token mapping belong to dark-mode.
Custom utilities and variants
@utility scrollbar-hidden {
scrollbar-width: none;
&::-webkit-scrollbar { display: none; }
}
@utility tab-* {
tab-size: --value(integer); /* tab-2, tab-4 */
}
@custom-variant pointer-coarse (@media (pointer: coarse));
Prefer @utility over a plain class in @layer components — utilities work with all
variants (hover:, md:) and sort correctly.
Component extraction
Order of preference when class lists repeat:
- A framework component (
<Button variant="primary">) that owns its classes. - A variant map with
cva/tailwind-variants, merged withtailwind-merge:
import { cva, type VariantProps } from "class-variance-authority";
import { twMerge } from "tailwind-merge";
export const button = cva(
"inline-flex items-center justify-center gap-2 rounded-lg font-medium transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
intent: {
primary: "bg-brand-500 text-white hover:bg-brand-700 focus-visible:outline-brand-500",
ghost: "bg-transparent text-fg hover:bg-fg/5",
},
size: { sm: "h-8 px-3 text-sm", md: "h-10 px-4 text-sm", lg: "h-12 px-6 text-base" },
},
defaultVariants: { intent: "primary", size: "md" },
},
);
export const buttonClass = (p: VariantProps<typeof button> & { className?: string }) =>
twMerge(button(p), p.className);
@applyonly for styling third-party markup you cannot add classes to, or content from a CMS. In Vue/Svelte/CSS-module<style>blocks, add@reference "../app.css";first so the block can see your theme.
Utility discipline
- Never build class names dynamically:
bg-${color}-500is invisible to the scanner. Map to complete strings:const tone = { red: "bg-red-500", green: "bg-green-500" }. - Mobile-first: unprefixed classes are the base,
md:/lg:add overrides. Do not writesm:for the base layout. - Use container queries for components that live in variable-width slots:
@containeron the parent,@md:grid-cols-2on the child. - Arbitrary values (
top-[117px]) for one-offs only; a value used twice becomes a token. - Reference variables with the v4 shorthand
bg-(--brand), notbg-[--brand]. - Important modifier goes at the end in v4:
hidden!. - Sort classes with
prettier-plugin-tailwindcss; in v4 set"tailwindStylesheet": "./src/app.css"in the Prettier config so it knows your theme.
Accessibility defaults
- Every interactive element gets a visible focus style:
focus-visible:outline-2 focus-visible:outline-offset-2(v4outline-hiddenkeeps an outline in forced-colors mode;outline-noneremoves it entirely). - Pair
sr-onlylabels with icon-only buttons. - Respect motion settings:
motion-safe:animate-fade-inormotion-reduce:transition-none. hover:in v4 applies only on devices that support hover; do not hide essential actions behind hover.
Gotchas
- Class missing in production but present in dev: dynamic class string, file outside
detected sources, or a package in
node_moduleswithout@source. - v4 default border color is
currentColor(v3 was gray-200); add an explicit color toborderutilities. - v4 targets Safari 16.4+, Chrome 111+, Firefox 128+. Older browser support means staying on v3.4.
- Do not combine Sass/Less with v4; Tailwind v4 is the preprocessor (nesting, imports, vendor prefixing via Lightning CSS).
tailwind-mergemust match the major Tailwind version (v3 of tailwind-merge for Tailwind v4).
Verification
- Production build, then search output CSS for a class you just added:
grep -o "bg-brand-500" dist/assets/*.css | head -1. - Toggle dark mode and check both themes for contrast (
accessibility-audithas a contrast script). - Resize from 320 px to wide desktop; no horizontal overflow
(
document.documentElement.scrollWidth > innerWidthin the console should be false). - Prettier with the Tailwind plugin produces no diff on touched files.
Bundled files
Loaded only when a step in the skill needs them. Review them here before installing.
references/v3-to-v4-migration.md
# Tailwind v3 to v4 migration
Read this before upgrading a project from Tailwind v3 to v4.
## Preconditions
- Node 20+ for the upgrade tool.
- Browser support must be Safari 16.4+, Chrome 111+, Firefox 128+. If the product
supports older browsers, stop and stay on v3.4.
- A clean git working tree, so the upgrade diff is reviewable.
- Screenshots or visual-regression baselines of key pages (see `visual-regression`).
## Procedure
```
- [ ] 1. Branch; capture baseline screenshots
- [ ] 2. npx @tailwindcss/upgrade (review the diff file by file)
- [ ] 3. Replace build integration (Vite plugin or @tailwindcss/postcss)
- [ ] 4. Move tailwind.config.js theme into @theme (or keep @config temporarily)
- [ ] 5. Fix renamed utilities the tool missed (dynamic strings, CMS content)
- [ ] 6. Fix default-value changes (border color, ring width, placeholder)
- [ ] 7. Re-add plugins via @plugin
- [ ] 8. Build, compare screenshots, fix diffs
```
The upgrade tool rewrites templates and CSS, but it cannot see class names built at
runtime, stored in databases, or used in files outside detected sources. Grep for those
afterwards.
## Build integration changes
| v3 | v4 |
|---|---|
| `tailwindcss` PostCSS plugin + `autoprefixer` | `@tailwindcss/postcss` (prefixing built in; remove autoprefixer and postcss-import) |
| Vite via PostCSS | `@tailwindcss/vite` plugin |
| `npx tailwindcss -i ... -o ...` | `npx @tailwindcss/cli -i ... -o ...` |
| `@tailwind base/components/utilities` | `@import "tailwindcss";` |
| `content: [...]` in config | Automatic detection; `@source` for extra paths |
| `plugins: [require("@tailwindcss/forms")]` | `@plugin "@tailwindcss/forms";` |
| `darkMode: "class"` | `@custom-variant dark (&:where(.dark, .dark *));` |
| `theme.extend.colors.brand` | `@theme { --color-brand-500: ...; }` |
## Renamed utilities
| v3 | v4 |
|---|---|
| `shadow-sm` | `shadow-xs` |
| `shadow` | `shadow-sm` |
| `drop-shadow-sm` / `drop-shadow` | `drop-shadow-xs` / `drop-shadow-sm` |
| `blur-sm` / `blur` | `blur-xs` / `blur-sm` |
| `rounded-sm` / `rounded` | `rounded-xs` / `rounded-sm` |
| `outline-none` | `outline-hidden` (the new `outline-none` truly sets `outline-style: none`) |
| `ring` (3px) | `ring-3` (bare `ring` is now 1px) |
| `bg-opacity-50`, `text-opacity-*` | `bg-black/50`, `text-white/75` |
| `flex-shrink-0` / `flex-grow` | `shrink-0` / `grow` |
| `overflow-ellipsis` | `text-ellipsis` |
| `decoration-slice` / `decoration-clone` | `box-decoration-slice` / `box-decoration-clone` |
| `bg-[--brand]` | `bg-(--brand)` |
| `!bg-red-500` | `bg-red-500!` |
## Behavior changes that look like bugs
- **Border and divide color** default to `currentColor` instead of gray-200. Add
explicit colors, or restore the old default in base styles:
```css
@layer base {
*, ::after, ::before, ::backdrop, ::file-selector-button { border-color: var(--color-gray-200, currentColor); }
}
```
- **Ring** default width 1px and color `currentColor` (was 3px blue-500).
- **Placeholder** text uses current color at 50% opacity (was gray-400).
- **Buttons** use `cursor: default` (browser default). Add `cursor-pointer` where wanted
or a base rule.
- **`space-x-*` / `space-y-*`** now use a different selector (margin on all but the last
child). Layouts with inline elements or reversed order can shift; prefer `gap-*` with
flex/grid.
- **Variant stacking** applies left to right: v3 `first:*:pt-0` becomes `*:first:pt-0`.
- **`hover:`** only applies under `@media (hover: hover)`; touch devices no longer get
sticky hover states.
- **`container`** utility has no `center`/`padding` options; customize with
`@utility container { margin-inline: auto; padding-inline: 1rem; }`.
- **Theme function** `theme(colors.red.500)` in CSS becomes `var(--color-red-500)`;
`theme()` still works with the CSS variable name but prefer variables.
- **`@apply` in component style blocks** (Vue, Svelte, CSS modules) needs
`@reference "<path to main css>";` at the top of the block.
## Keeping the JS config temporarily
Large configs can stay as-is during the first pass:
```css
@import "tailwindcss";
@config "../tailwind.config.js";
```
`corePlugins`, `safelist`, and `separator` options are not supported in v4. Replace
`safelist` with `@source inline("bg-red-500 bg-green-500")` (v4.1+) or with
complete class strings in source.
## Done when
- Production build passes and screenshots match baselines, or each diff is a reviewed,
intended change.
- `grep -rn "bg-opacity\|flex-shrink-\|flex-grow\|@tailwind " src` returns nothing.
- `tailwind-merge`, `prettier-plugin-tailwindcss`, and editor IntelliSense are on
versions that support v4.
About this skill
Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.