Frontend No. 068

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.

  • nametailwind-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.

~/.claude/skills/tailwind-css/SKILL.md

--- name: tailwind-css description: Styles interfaces with Tailwind CSS v4 using CSS-first configuration — @import "tailwindcss", @theme design tokens, @utility and… license: MIT ---

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
FindingMeaning
@tailwind base; @tailwind components; @tailwind utilities;v3 — keep v3 conventions or migrate deliberately
@import "tailwindcss"; + @themev4 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 (default 0.25rem): p-13, w-130 work without config. Prefer the scale over arbitrary p-[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:

  1. A framework component (<Button variant="primary">) that owns its classes.
  2. A variant map with cva / tailwind-variants, merged with tailwind-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);
  1. @apply only 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}-500 is 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 write sm: for the base layout.
  • Use container queries for components that live in variable-width slots: @container on the parent, @md:grid-cols-2 on 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), not bg-[--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 (v4 outline-hidden keeps an outline in forced-colors mode; outline-none removes it entirely).
  • Pair sr-only labels with icon-only buttons.
  • Respect motion settings: motion-safe:animate-fade-in or motion-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_modules without @source.
  • v4 default border color is currentColor (v3 was gray-200); add an explicit color to border utilities.
  • 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-merge must match the major Tailwind version (v3 of tailwind-merge for Tailwind v4).

Verification

  1. Production build, then search output CSS for a class you just added: grep -o "bg-brand-500" dist/assets/*.css | head -1.
  2. Toggle dark mode and check both themes for contrast (accessibility-audit has a contrast script).
  3. Resize from 320 px to wide desktop; no horizontal overflow (document.documentElement.scrollWidth > innerWidth in the console should be false).
  4. 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 108 lines · 4.6 KB
# 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.

esc