tailwind-v4

v2026.09.25

Tailwind CSS v4 conventions for real projects — CSS-first configuration with @theme, automatic content detection, the v3-to-v4 migration diffs, and dynamic class-name safety. Use when writing Tailwind utilities, setting up or upgrading Tailwind, defining design tokens as Tailwind theme values, debugging classes that do not generate, or when unsure whether a project is on v3 or v4.

GitHub
安装命令
npx skhub add boraoztunc/tailwind-v4
Markdown
SKILL.md

Tailwind CSS v4

Detect the version first

This is the highest-value step. v3 and v4 configure completely differently, and guidance for one is actively wrong for the other.

# Check the installed major version
npm ls tailwindcss --depth=0 2>/dev/null || grep '"tailwindcss"' package.json
SignalVersion
@import "tailwindcss"; in CSSv4
@tailwind base; @tailwind components; @tailwind utilities;v3
@theme { } block in CSSv4
tailwind.config.js with a content: [...] arrayv3 (or a v4 project using the compat layer)
@config "./tailwind.config.js";v4 loading a legacy config
@tailwindcss/postcss or @tailwindcss/vite dependencyv4
autoprefixer + postcss-import required in postcss configv3

Never write a tailwind.config.js for a v4 project by default. v4 reads configuration from CSS. A hand-added config file is silently ignored unless explicitly loaded with @config.

v4 setup

/* app.css — the single entry point */
@import "tailwindcss";

That one line replaces the three @tailwind directives, and it also handles postcss-import and autoprefixer internally — remove both from the PostCSS config.

Vite:

// vite.config.js
import tailwindcss from "@tailwindcss/vite";
export default { plugins: [tailwindcss()] };

PostCSS:

// postcss.config.mjs
export default { plugins: { "@tailwindcss/postcss": {} } };

Configuration is CSS-first

Design tokens go in @theme. Every entry generates the matching utilities and exposes a real CSS custom property.

@import "tailwindcss";

@theme {
  --color-brand: oklch(0.62 0.19 250);
  --color-brand-muted: oklch(0.72 0.08 250);
  --font-display: "IBM Plex Sans", sans-serif;
  --radius-card: 14px;
  --spacing-gutter: 2.75rem;
  --ease-out-soft: cubic-bezier(0.22, 0.61, 0.36, 1);
}

--color-brand yields bg-brand, text-brand, border-brand, … and is readable anywhere as var(--color-brand). That dual nature is the main v4 win: tokens are no longer trapped inside a JS config.

Namespaces map to utility families — --color-*, --font-*, --text-*, --spacing-*, --radius-*, --shadow-*, --ease-*, --animate-*, --breakpoint-*.

Override a whole namespace, or everything:

@theme {
  --color-*: initial;          /* drop the default palette */
  --color-ink: #18181b;
}

No content array. v4 detects source files automatically, honoring .gitignore and skipping binaries. Point it at extra sources only when needed:

@source "../packages/ui/src";           /* add a path outside the project root */
@source not "../legacy";                 /* exclude */
@source inline("bg-red-500 bg-red-600"); /* the v4 replacement for safelist */

Migrating v3 → v4

Run the codemod first, then review the diff:

npx @tailwindcss/upgrade

The renames that break silently:

v3v4
shadow-smshadow-xs
shadowshadow-sm
rounded-smrounded-xs
roundedrounded-sm
blur-smblur-xs
outline-noneoutline-hidden
bg-opacity-50bg-black/50 (slash opacity only)
flex-shrink-0shrink-0

The bare shadow → shadow-sm and rounded → rounded-sm shifts are the dangerous pair: the old class names still exist but now mean one step smaller, so a v3 codebase silently loses a step of elevation and radius everywhere rather than erroring.

Other behavior changes to expect:

  • Default border color is now currentColor, not gray-200. border alone inherits text color — set the color explicitly.
  • Default ring is 1px currentColor, was 3px blue. ring → ring-3 to keep v3 sizing.
  • Space-between uses a different selector; sibling-margin edge cases can shift.
  • Hover only applies where the device actually supports hover.
  • Variant stacking now reads left-to-right.

Dynamic class names

The rule survives from v3, because the scanner still reads source text statically:

// BROKEN — never generated
<div className={`text-${color}-500`} />

// CORRECT — complete class names, statically visible
const TONE = {
  danger: "text-red-500",
  success: "text-emerald-500",
};
<div className={TONE[tone]} />

v4 does support dynamic values on many utilities without config — grid-cols-15, w-17, mt-23 work arbitrarily. What cannot be assembled is the class name string. For genuinely runtime-only values, use an inline style or a CSS variable, not string concatenation:

<div className="w-(--w)" style={{ "--w": `${pct}%` }} />

Practical patterns

Dark mode. v4 defaults to prefers-color-scheme. For class-based toggling:

@custom-variant dark (&:where(.dark, .dark *));

Component extraction order. Prefer a real component over @apply. When you do need shared CSS, v4 wants @utility:

@utility card {
  border-radius: var(--radius-card);
  background: var(--color-surface);
}

Heavy @apply use forfeits the point of utilities and inflates CSS — reach for it last, not first.

Arbitrary values stay available for one-offs: top-[117px], bg-[oklch(0.7_0.1_200)], grid-cols-[1fr_auto].

Common failures

  • Classes not generating → a dynamically-built class name, or a source file outside auto-detection (add @source).
  • A tailwind.config.js having no effect → v4 project without @config.
  • Build errors after upgrade → postcss-import/autoprefixer still in the PostCSS chain; v4 includes both.
  • Borders/rings looking wrong post-migration → the currentColor and ring-1 default changes above.
  • Elevation subtly flattened post-migration → the shadow/rounded rename shift.

Related skills

  • visual-style-presets — pick a direction, then encode its tokens in @theme
  • beautiful-shadows — exact layered shadows as arbitrary utilities
发现
标签

此技能尚未发布标签。

版本
最新版本元数据

版本

v2026.09.25

发布时间

Sep 25, 2026

分类

未分类

许可证

Apache-2.0

源路径

tailwind-v4

默认分支

main

最新提交

645553c

Tree SHA

4ed7ca6