Problem
After migrating to Tailwind v4, the build fails with a CssSyntaxError when a @custom-variant declaration is nested inside @layer or another at-rule:
CssSyntaxError: tailwindcss: @custom-variant cannot be nested.
> 1 | @layer utilities {
2 | @custom-variant pointer-coarse (@media (pointer: coarse));
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
3 | }
at /app/src/styles/globals.css:2:3
The broken stylesheet that triggers the error:
@import "tailwindcss";
@layer utilities {
@custom-variant pointer-coarse (@media (pointer: coarse));
@custom-variant dark-mode (&:where(.dark, .dark *));
}
Solution
Move @custom-variant declarations to the root level of the stylesheet, outside any @layer or nested block:
@import "tailwindcss";
@custom-variant pointer-coarse (@media (pointer: coarse));
@custom-variant dark-mode (&:where(.dark, .dark *));
If you have multiple custom variants mixed with utility definitions, separate them:
@import "tailwindcss";
/* Custom variants must be at root level */
@custom-variant pointer-coarse (@media (pointer: coarse));
@custom-variant dark-mode (&:where(.dark, .dark *));
/* Utilities can remain inside @layer or @utility */
@utility container-narrow {
max-width: 48rem;
margin-inline: auto;
}
Why It Works
Tailwind v4 resolves variant definitions during an early pass of the CSS parser, before @layer ordering and rule nesting are processed. The @custom-variant directive must be visible at the top level so the engine can register it as a variant before any utility classes referencing it are generated. Nesting it inside @layer or another at-rule puts it in a scope the variant resolver cannot reach, causing the parse failure.
Context
- Tailwind CSS v4.0+ with the new CSS-first configuration model
- In Tailwind v3, custom variants were defined in
tailwind.config.jsviaaddVariant()-- there was no CSS-level equivalent to nest incorrectly - The same top-level requirement applies to
@themeand@sourcedirectives in v4 - If using PostCSS with
@tailwindcss/postcss, the error surfaces during the PostCSS transform step - The
@tailwindcss/upgradecodemod handles most migrations but may miss custom variants that were manually added to CSS files after initial setup - This also applies to
@custom-variantwith block syntax:@custom-variant dark-mode { &:where(.dark, .dark *) { @slot; } }