| name | tailwind-v4-expert |
| description | Use when migrating Tailwind v3 to v4, configuring CSS-first @theme tokens, debugging Oxide engine errors, dealing with the absence of tailwind.config.js, container queries (@container) usage, layer cascade problems, or "dynamic class names not generating". Triggers: switching from `@tailwind base` to `@import "tailwindcss"`, RSC/Next.js streaming + CSS ordering, dark-mode strategy choices, custom variants via @variant, content-scanning glob tuning. NOT for Tailwind v3 (different config model), CSS-in-JS frameworks, vanilla CSS architecture (BEM/SMACSS), or react-native styling. |
| category | Frontend & UI |
| tags | ["tailwind","css","design-tokens","oxide","container-queries","frontend"] |
Tailwind v4 Expert
Tailwind v4 is a different tool than v3. The PostCSS-based JIT engine is gone, replaced by Oxide (Rust). The JS config file is gone, replaced by CSS-first @theme blocks. Plugins from v3 mostly need rewriting. If you're migrating, expect to rebuild — not patch — your config layer.
When to use
- Migrating a v3 project (look for
@tailwind base; @tailwind components; @tailwind utilities;).
- Need design tokens (colors, fonts, breakpoints) consumed by both Tailwind and raw CSS.
- Container queries — designing components that adapt to their container, not the viewport.
- Custom variants beyond what v4 ships (
@variant focus-within-sibling, etc.).
- Dynamic class names disappearing from the build (Oxide can't see what isn't statically present).
- RSC + streaming + Tailwind ordering bugs.
Core capabilities
The v4 import model
@import "tailwindcss";
@source "../../packages/ui/src/**/*.{ts,tsx}";
@source "../../node_modules/@my-org/icons/dist/**/*.js";
There is no tailwind.config.js. There is no content array. @source adds globs to the default scan root (the file's dir + project conventions). Oxide statically scans these for class strings.
@theme — design tokens are CSS now
@theme {
--color-brand-50: oklch(97% 0.02 250);
--color-brand-500: oklch(60% 0.18 250);
--color-brand-900: oklch(20% 0.10 250);
--font-display: "Söhne", "Inter", sans-serif;
--font-mono: "JetBrains Mono", ui-monospace, monospace;
--breakpoint-3xl: 120rem;
--spacing-128: 32rem;
}
These tokens generate utilities (text-brand-500, font-display, 3xl:, w-128) AND are accessible as var(--color-brand-500) in raw CSS. One source of truth.
Namespaces that auto-generate utilities:
| Token prefix | Generates |
|---|
--color-* | text-*, bg-*, border-*, outline-*, etc. |
--font-* | font-* |
--text-* | text-* (font-size; pair with --text-*--line-height) |
--spacing-* | m-*, p-*, w-*, h-*, gap-*, etc. |
--breakpoint-* | sm:, md:, lg:… variants |
--radius-* | rounded-* |
--shadow-* | shadow-* |
@layer cascade
@layer base {
body { @apply font-sans text-zinc-900; }
}
@layer components {
.btn { @apply rounded-lg px-4 py-2 font-semibold; }
}
@layer my-overrides {
.prose img { border-radius: var(--radius-lg); }
}
Layers cascade: base → components → utilities → arbitrary user layers. Tailwind defines its own at @layer base, components, utilities; automatically; you rarely need to redeclare them.
Custom variants
@variant pointer (@media (pointer: fine));
@variant hocus (&:hover, &:focus-visible);
@variant aria-current (&[aria-current="page"]);
Then: <a class="text-zinc-500 hocus:text-brand-500 aria-current:text-brand-700">.
Container queries
<aside class="@container">
<article class="grid grid-cols-1 @lg:grid-cols-2 @3xl:grid-cols-3">
Named containers if the same element has nested ones:
<main class="@container/main">
<aside class="@container/sidebar">
<div class="@md/sidebar:hidden @lg/main:block">
Container query breakpoints come from --container-* tokens (separate namespace from --breakpoint-*).
Dark mode
Three strategies; pick one:
@variant dark (@media (prefers-color-scheme: dark));
@variant dark (&:where(.dark, .dark *));
@variant dark (&:where([data-theme="dark"], [data-theme="dark"] *));
Then theme-shift colors via light-dark() or via separate token blocks:
@theme {
--color-bg: oklch(99% 0 0);
}
@layer base {
:where(.dark) {
--color-bg: oklch(15% 0 0);
}
}
Next.js App Router specifics
- Put
@import "tailwindcss" and @theme in app/globals.css.
- Import that file once in
app/layout.tsx.
- For RSC streaming, ensure the CSS link is in
<head> (Next does this automatically when imported from layout).
- Server Components can use Tailwind classes directly; no client boundary needed.
- If you ship a workspace
@my-org/ui package, add @source "../../packages/ui/**/*.{ts,tsx}" to your globals so Oxide scans it.
Anti-patterns
Dynamic class names that Oxide can't see
Symptom: text-${color}-500 works in dev, missing in production.
Diagnosis: Oxide is a static scanner — it sees string literals, not template strings.
Fix: Map the dynamic value to a literal class:
const variants = { primary: 'bg-brand-500', danger: 'bg-red-500' } as const;
<button class={variants[kind]} />
Or use CSS variables for the dynamic part: style={{ '--accent': color }} class="bg-[--accent]".
Migrating but keeping tailwind.config.js
Symptom: v4 builds, but custom theme tokens don't appear; warnings about unknown directives.
Diagnosis: v4 ignores tailwind.config.js. The presence of one suggests an incomplete migration.
Fix: Move theme.extend values into @theme, plugins into @variant/CSS, content into @source. Delete the file. Run npx @tailwindcss/upgrade@latest for an automated first pass.
@apply for everything
Symptom: CSS bundle bigger than v3, "components" file is 2000 lines of @apply.
Diagnosis: Each @apply inlines utilities; deeply-applied components don't dedupe well.
Fix: Apply once at the component boundary; compose via class strings (clsx/cva) for variants.
Forgetting @source for files outside the project root
Symptom: UI library shipped from a sibling workspace — its classes don't appear in production CSS.
Diagnosis: Default scan covers the project's own files. Workspace deps need explicit @source.
Fix: @source "../../packages/ui/src/**/*.{ts,tsx}" (relative to the CSS file, not the project root).
PurgeCSS-style mental model
Symptom: Engineers add classes to a "safelist" that doesn't exist.
Diagnosis: v4 doesn't purge — it generates. There's no allowlist; if the class isn't in a scanned file, it doesn't exist.
Fix: Use a stub component or comment that contains the literal classes:
const STATUS_COLORS = { error: 'bg-red-500', ok: 'bg-green-500', info: 'bg-blue-500' };
v3 plugins copy-pasted into v4
Symptom: Plugin "tailwindcss/typography" failed to load.
Diagnosis: v4's plugin API is incompatible with most v3 plugins.
Fix: Use the v4-native plugin (@tailwindcss/typography v0.6+ for v4), or rewrite the plugin's effects with @variant / @layer components / custom utilities.
Quality gates
NOT for
- Tailwind v3 — different config model, different engine.
- CSS-in-JS (Emotion, styled-components) — pair with css-in-js-architect skill.
- Vanilla CSS architecture (BEM, SMACSS, ITCSS) — different paradigm.
- React Native — Tailwind variants for RN exist but have different constraints.
- CSS modules / vanilla-extract — competing solutions.