Tailwind CSS v4 CSS-First Configuration
v4 moves the entire configuration surface from JavaScript into CSS.
Tokens are CSS variables in @theme. Custom utilities are CSS at-rules.
Plugins, variants, and content paths all declare in CSS. The JS config
file is opt-in only, via @config "./tailwind.config.js", for migration.
Companion skills :
tailwind-impl-config-v3: the v3 JS-config surface (tailwind.config.js)tailwind-core-v3-vs-v4: v3-to-v4 differences across the APItailwind-impl-migration-v3-v4: automated codemod + manual stepstailwind-errors-v4-migration: trap catalogue when an upgrade breaks
Quick Reference : The Minimum v4 Setup
/* src/app.css */
@import "tailwindcss";
That single line replaces every v3 setup step (no JS config, no PostCSS
plugin chain, no @tailwind directives, no postcss-import, no
autoprefixer). Add tokens, plugins, sources, and custom utilities
below as needed.
Directive Index
| Directive | Purpose | One-line example |
|---|---|---|
@import "tailwindcss" |
Load base, components, utilities | @import "tailwindcss"; |
@import "tailwindcss/preflight" |
Just the reset | partial install |
@import "tailwindcss/utilities" |
Just the utilities | partial install |
@import "tailwindcss/theme" |
Just the default theme | partial install |
@theme { ... } |
Define design tokens as CSS variables | @theme { --color-brand: oklch(0.72 0.11 178); } |
@theme inline { ... } |
Resolve var refs at build, inline value | @theme inline { --font-sans: var(--font-inter); } |
@theme static { ... } |
Always emit CSS variables, even if unused | @theme static { --color-x: red; } |
@source "<glob>" |
Add a content-scan path | @source "../packages/ui"; |
@source not "<glob>" |
Exclude a path | @source not "../src/legacy"; |
@source inline("...") |
Safelist with brace expansion | @source inline("bg-{red,blue}-500"); |
@source none |
Disable automatic detection entirely | @source none; |
@plugin "name" |
Load a legacy JS plugin | @plugin "@tailwindcss/typography"; |
@utility name { ... } |
Define a custom utility | @utility tab-4 { tab-size: 4; } |
@variant name { ... } |
Apply a variant inside CSS | .x { @variant dark { background: black; } } |
@custom-variant name (...) |
Register a custom variant | @custom-variant dark (&:where(.dark, .dark *)); |
@reference "<file>" |
Import theme into a scoped stylesheet | @reference "../app.css"; |
@config "<path>" |
Load legacy v3 JS config | @config "./tailwind.config.js"; |
@apply |
Inline utilities into a rule | .btn { @apply px-4 py-2; } |
@layer base/components/utilities |
Cascade-layer organisation | @layer base { h1 { ... } } |
NEVER use the v3 @tailwind base/components/utilities directives in v4.
They are removed. Single @import "tailwindcss" replaces all three.
@import : Partial Loading
/* Full install (default for most projects) */
@import "tailwindcss";
/* Just the preflight reset, no utilities */
@import "tailwindcss/preflight";
/* Just utilities, no preflight (rare, but possible) */
@import "tailwindcss/utilities";
/* Just the default theme variables */
@import "tailwindcss/theme";
ALWAYS use the full @import "tailwindcss" for application code. Partial
imports exist for embedding Tailwind inside a third-party context where
you control exactly which layers to ship.
@theme : Design Tokens as CSS Variables
@import "tailwindcss";
@theme {
/* Custom color (creates bg-mint-500, text-mint-500, etc.) */
--color-mint-500: oklch(0.72 0.11 178);
/* Custom font (creates font-display) */
--font-display: "Inter Variable", "Inter", system-ui, sans-serif;
/* Custom breakpoint (creates 3xl: variant) */
--breakpoint-3xl: 120rem;
/* Custom spacing key */
--spacing-128: 32rem;
}
A token named --{namespace}-{key} automatically generates utilities. The
namespace prefix determines which utility family. Every token also becomes
a CSS custom property accessible via var(--color-mint-500).
Theme Namespaces
The full namespace table (~20 namespaces, every utility-family pairing) lives in references/methods.md. The most-used namespaces are --color-*, --font-*, --text-*, --spacing / --spacing-*, --breakpoint-*, --radius-*, --shadow-*. Add a token in the matching namespace and the matching utility family appears automatically.
NEVER prefix a token with a namespace Tailwind does not recognise. A token
--brand-blue: blue will become var(--brand-blue) but generates NO
utility class. Use --color-brand-blue to get bg-brand-blue,
text-brand-blue, and the rest.
Source : https://tailwindcss.com/docs/theme
@theme inline vs @theme static vs default
@theme {
/* Default : creates utility class that references var(--color-x) */
--color-x: oklch(0.72 0.11 178);
}
@theme inline {
/* Inline : resolves var refs at build, embeds the resolved value
in the utility class. Use when a token references ANOTHER var. */
--font-sans: var(--font-inter);
}
@theme static {
/* Static : always emits the CSS variable in output, even when no
utility uses it (default behaviour omits unused tokens). */
--color-primary: var(--color-red-500);
}
ALWAYS use @theme inline when a token's value contains another
var(...) reference. The default behaviour produces a chain of
references that may resolve to nothing if the outer scope changes.
ALWAYS use @theme static for tokens consumed only from JavaScript
(via getComputedStyle) where pruning would hide them.
Resetting a Namespace
@theme {
--color-*: initial; /* drop all default colours */
--color-brand: #1da1f2;
/* --*: initial; drops EVERY default token */
}
@source : Content Detection
v4 auto-scans project files for class-name tokens. Defaults skip
.gitignored files, node_modules, binaries, CSS files, and lockfiles.
/* Add a sibling package */
@source "../packages/ui/src/**/*.{html,ts,tsx,vue,svelte}";
/* Add a node_modules path (default scanner ignores node_modules) */
@source "../node_modules/@my-org/ui-lib";
/* Exclude */
@source not "../src/legacy";
@source not "../tests/**/*.fixture.html";
/* Disable automatic scanning entirely (rare) */
@source none;
Safelist via @source inline
/* Static list */
@source inline("bg-red-500 bg-green-500 bg-blue-500");
/* Brace expansion */
@source inline("bg-{red,green,blue}-500");
/* Range expansion (start..end..step) */
@source inline("p-{1..12}");
@source inline("opacity-{0..100..10}");
/* With variants */
@source inline("{hover:,focus:,}bg-{red,blue}-{50,{100..900..100},950}");
ALWAYS use @source inline for dynamic class names built from runtime
data (bg-${color}-500). NEVER rely on the scanner to find these : the
scanner is plain-text tokenisation and cannot follow string interpolation.
Source : https://tailwindcss.com/docs/detecting-classes-in-source-files
@plugin : Legacy JS Plugins
@import "tailwindcss";
@plugin "@tailwindcss/typography";
@plugin "@tailwindcss/forms";
@plugin "@tailwindcss/aspect-ratio";
/* Local plugin file */
@plugin "./plugins/scrollbar.js";
The plugin file follows the v3 plugin API (CommonJS export of
plugin(function({ addUtilities, addVariant, ... }))). v4 evaluates the
plugin at build time and merges its utilities, variants, and base styles
into the v4 cascade.
For new code, ALWAYS prefer @utility and @custom-variant over JS
plugins. Use @plugin for back-compat or for plugins that need JS-side
config logic (like Typography's theme() introspection).
@utility : Custom Utilities
Simple form
@utility content-auto {
content-visibility: auto;
}
@utility scrollbar-thin {
scrollbar-width: thin;
&::-webkit-scrollbar {
width: 6px;
}
}
Each @utility block defines ONE class name. The class works with every
variant : hover:content-auto, md:scrollbar-thin, dark:content-auto.
Parameterised utilities
/* Generates tab-1, tab-2, tab-76, etc. */
@utility tab-* {
tab-size: --value(integer);
}
/* Reads from a theme namespace */
@theme {
--tab-size-github: 8;
--tab-size-2: 2;
--tab-size-4: 4;
}
@utility tab-* {
tab-size: --value(--tab-size-*);
}
Helper functions inside @utility
| Helper | Purpose |
|---|---|
--value(integer) |
Match tab-2, tab-76 (plain integer arg) |
--value([integer]) |
Match tab-[1] (arbitrary value) |
--value(--tab-size-*) |
Match tab-github (theme key) |
--value([length]) |
Match --value([5rem]) length values |
--value(integer, --default(4)) |
With default for bare tab |
--modifier(--leading-*, [length]) |
Read modifier (text-lg/6 modifier is 6) |
--alpha(var(--color-lime-300) / 50%) |
Compose alpha via color-mix(in oklab, X 50%, transparent) |
--spacing(4) |
calc(var(--spacing) * 4) |
/* Combined : matches text-lg, text-lg/6, text-[14px], text-[14px]/6 */
@utility text-* {
font-size: --value(--text-*, [length]);
line-height: --modifier(--leading-*, [length], [*]);
}
/* Negative parameterised utility */
@utility -inset-* {
inset: calc(--value([percentage], [length]) * -1);
}
Source : https://tailwindcss.com/docs/adding-custom-styles
@variant : Apply a Variant Inside CSS
.btn {
background: white;
color: black;
@variant dark {
background: black;
color: white;
}
@variant hover, focus {
background: gray;
}
}
Useful when you want to apply variant scoping to plain CSS without
writing a Tailwind class. ALWAYS prefer utility classes for new code ;
@variant is for legacy CSS being migrated to v4 theme tokens.
@custom-variant : Define a New Variant
/* Block form */
@custom-variant theme-midnight {
&:where([data-theme="midnight"] *) {
@slot;
}
}
/* Shorthand form */
@custom-variant theme-midnight (&:where([data-theme="midnight"] *));
@custom-variant pointer-coarse (@media (pointer: coarse));
@custom-variant high-contrast (@media (prefers-contrast: more));
@custom-variant dark (&:where(.dark, .dark *));
Then use like any built-in variant :
<button class="bg-white theme-midnight:bg-black pointer-coarse:p-4"></button>
v3 needed a JS plugin with addVariant(). v4 makes this a one-liner.
@reference : Scoped @apply (Vue/Svelte/CSS-modules trap)
<!-- Vue SFC with scoped style -->
<style scoped>
@reference "../app.css";
.btn {
@apply px-4 py-2 bg-blue-500 text-white;
}
</style>
<!-- Svelte component -->
<style>
@reference "../app.css";
.btn {
@apply px-4 py-2 bg-blue-500 text-white;
}
</style>
Without @reference, v4 compiles each scoped block in isolation and
throws "Cannot apply unknown utility class". The @reference directive
imports the theme + custom utilities WITHOUT duplicating the CSS output.
Use @reference "tailwindcss" for projects with no custom theme :
@reference "tailwindcss";
.btn {
@apply px-4 py-2;
}
ALWAYS add @reference to every scoped <style> block in Vue, Svelte,
Astro, and CSS-module projects. NEVER use @import "tailwindcss" inside
a scoped block ; that duplicates the entire framework into every
component's CSS output.
Source : https://tailwindcss.com/docs/functions-and-directives
@config : Legacy JS Config Back-Compat
@import "tailwindcss";
@config "../tailwind.config.js";
Loads a v3-style tailwind.config.js and merges its theme.extend,
plugins, and content paths into v4. Removed v3 options
(corePlugins, safelist, separator, resolveConfig) are silently
ignored. Use this for incremental migration only.
NEVER define the same token in BOTH @theme AND the JS config's
theme.extend. Behaviour is unspecified ; the safer rule is
"migrate per token category, in one commit".
@apply : Inline Utilities
@layer components {
.btn {
@apply px-4 py-2 rounded-md bg-blue-500 text-white;
}
.card {
@apply rounded-lg p-6 shadow-md;
}
}
ALWAYS wrap @apply blocks in @layer components (or another layer) so
utility classes from the user's HTML can still override them. NEVER
declare apply rules at the top level ; they then sit in the same cascade
layer as utilities and lose to almost nothing.
@apply needs theme context. In scoped contexts (Vue, Svelte, CSS
modules) add @reference first ; see the section above.
@layer : Cascade-Layer Organisation
@import "tailwindcss";
@layer base {
h1 { font-size: var(--text-3xl); font-weight: bold; }
h2 { font-size: var(--text-2xl); }
}
@layer components {
.card {
background: var(--color-white);
border-radius: var(--radius-lg);
padding: --spacing(6);
}
}
@layer utilities {
.text-balance { text-wrap: balance; }
}
v4 uses native CSS @layer (cascade layers) instead of v3's synthetic
ordering. Layers cascade in declared order : theme < base < components < utilities. Utilities ALWAYS win over components ; components win over
base.
Complete v4 Stylesheet Example
See references/examples.md for a full annotated app.css that combines @theme, @plugin, @source, @custom-variant, @utility, and @layer into a production-grade configuration.
Reference Files
references/methods.md: every directive with full syntax, every theme namespace, every --value helper, browser baselinereferences/examples.md: minimal + full stylesheet, scoped styles setup, partial-import use cases, dynamic class safelist, framework integrations (Vite, PostCSS, Next.js, Astro)references/anti-patterns.md: silent token drop, missing @reference, hard-coded namespace, @apply outside @layer, JS config mixed with @theme
Verified Sources
- https://tailwindcss.com/docs/functions-and-directives (every directive)
- https://tailwindcss.com/docs/theme (namespaces, inline, static)
- https://tailwindcss.com/docs/adding-custom-styles (@utility, @layer)
- https://tailwindcss.com/docs/detecting-classes-in-source-files (@source)
- https://tailwindcss.com/blog/tailwindcss-v4 (rationale + overview)