Dans Tailwind v4, --z-overlay: 200 à l'intérieur de @theme ne crée pas d'utilitaire z-overlay. Les utilitaires lisent des namespaces, et celui de z- est --z-index-. Rien ne plante et rien n'avertit. La classe n'existe tout simplement pas, l'élément retombe sur z-index: auto, et tout ce qui a un vrai z-index positif se peint par-dessus — sur certaines routes, pas sur d'autres. La vérification qui tranche vraiment, c'est de grepper le nom de la classe dans le CSS compilé, pas dans les sources.
Le symptôme
Nav admin mobile dans mon template de réservation. Je tape sur le hamburger, le drawer entre en glissant — et le backdrop assombrissant se pose au-dessus. On voit le drawer à travers le scrim, teinté et flouté, et chaque tap à l'intérieur ferme le menu au lieu de naviguer.
Sur desktop, tout allait bien. Uniquement en dessous du breakpoint md. Le build passait, le lint passait, les tests passaient.
Les deux éléments vivent dans le même fichier, src/components/admin/AdminSidebar.tsx, à quelques dizaines de lignes d'écart :
// line 221 — the drawer panel
"fixed inset-y-0 start-0 z-overlay w-64 -translate-x-full transition-all duration-200 ease-out",
"md:static md:translate-x-0 md:z-auto",// line 248 — the backdrop
className="md:hidden fixed inset-0 z-40 bg-black/40 backdrop-blur-sm"200 est supérieur à 40. C'est le drawer qui devrait gagner.
Ce que j'ai supposé
Je supposais que z-overlay se résolvait en 200, parce que j'avais écrit le token moi-même dans src/app/globals.css :
@theme inline {
--z-overlay: 200;
}Donc la valeur n'était pas en cause, et je suis parti chasser un stacking context à la place — un transform, un backdrop-filter, un will-change sur un ancêtre quelconque qui créerait un nouveau contexte et y enfermerait le z-index du drawer. C'est la réponse habituelle à « mon z-index est correct et il est ignoré », et j'y ai brûlé du temps pour de vrai. Le backdrop-blur-sm posé juste là sur le scrim rendait la piste plausible.
Mauvaise question. J'avais regardé une fois le style calculé du drawer, vu z-index: auto, et je l'ai lu comme « le stacking context l'avale » plutôt que comme l'explication plus simple : il n'y avait aucune règle à appliquer. Je n'ai jamais vérifié si z-overlay était une classe tout court.
Le vrai mécanisme : les namespaces de thème
Tailwind v4 a sorti la configuration de tailwind.config.js pour la mettre dans le CSS (@config survit comme échappatoire de compatibilité), et le pont entre une variable CSS et un utilitaire, c'est le namespace de thème. Le préfixe du nom de la variable décide quelle famille d'utilitaires elle alimente :
--color-*→bg-*,text-*,border-*,fill-*, …--spacing-*→p-*,m-*,gap-*,w-*, …--radius-*→rounded-*--tracking-*→tracking-*--z-index-*→z-*
Ça se lit directement dans le package installé. Dans node_modules/tailwindcss/dist/lib.mjs (v4.3.2), l'utilitaire z est enregistré comme ceci — le fichier est minifié, donc les retours à la ligne sont de moi :
n("z",{supportsNegative:!0,handleBareValue:({value:a})=>I(a)?a:null,
themeKeys:["--z-index"],handle:a=>[o("z-index",a)],
staticValues:{auto:[o("z-index","auto")]}})themeKeys: ["--z-index"]. Ce tableau est la liste complète des préfixes de variables que z-* va regarder. --z-overlay n'y est pas, donc z-overlay n'est pas un candidat — il n'y a aucun utilitaire à générer.
Sortez le même champ pour chaque utilitaire et vous obtenez l'essentiel de l'inventaire des namespaces :
grep -o 'themeKeys:\[[^]]*\]' node_modules/tailwindcss/dist/lib.mjs \
| grep -o -- '--[a-z-]*' | sort -u61 namespaces sur la 4.3.2. Deux réserves avant de prendre ça pour parole d'évangile : le pattern plus étroit themeKeys:\["--[a-z-]*"\] n'attrape que les utilitaires qui lisent une seule clé et en renvoie 43, et quelques namespaces — --spacing au premier chef — sont résolus par des chemins de code séparés et n'apparaissent jamais dans un tableau themeKeys. C'est donc un test au doigt mouillé, pas la spec. Ce qui compte tient dans les deux cas : la liste est finie, elle est décidée au build, et --z-overlay n'y figure pas. Une variable dont le préfixe n'est pas un namespace n'est, pour le moteur d'utilitaires, qu'une variable.
Vous pouvez le regarder se produire sans navigateur, en utilisant directement l'API du compilateur :
import { compile } from 'tailwindcss'
const css = `@theme static {
--z-overlay: 200;
--z-index-modal: 300;
}
@tailwind utilities;`
const c = await compile(css, { base: process.cwd() })
console.log(c.build(['z-overlay', 'z-modal']))Sortie :
/*! tailwindcss v4.3.2 | MIT License | https://tailwindcss.com */
:root, :host {
--z-overlay: 200;
--z-index-modal: 300;
}
.z-modal {
z-index: var(--z-index-modal);
}Les deux variables sont émises. Une seule classe l'est. J'ai utilisé @theme static ici délibérément, parce que ce mode est le plus trompeur des trois : la variable atterrit dans :root, donc vous pouvez l'inspecter dans les DevTools, confirmer --z-overlay: 200, et conclure que le token est en bonne santé alors que l'utilitaire n'existe pas. Le @theme simple élague la variable inutilisée, et @theme inline — c'est ce qu'utilise mon globals.css — n'émet ni la variable ni la classe. Tous les modes sont silencieux ; ils diffèrent seulement par la dose de fausse assurance qu'ils vous donnent.
Le mode de défaillance est pire que « pas de z-index ». Un élément positionné avec z-index: auto n'est promu nulle part — il se peint dans l'ordre de l'arbre, au milieu des autres éléments positionnés mais non empilés. Un élément avec n'importe quel z-index positif se peint dans une passe ultérieure, inconditionnellement au-dessus de tous. Donc z-40 bat un z-overlay cassé quelle que soit la position de l'un ou de l'autre dans le DOM — et sur les routes où rien d'autre ne déclare de z-index positif, la même classe cassée a l'air de marcher, parce que l'ordre de l'arbre tombe bien. C'est pour ça que ça se lisait comme une bizarrerie de rendu propre à une route plutôt que comme une classe manquante.
Le correctif
Un renommage de la variable, et rien d'autre. src/app/globals.css :
@theme inline {
/* ── Z-index overlay layer ─────────────────────────────────────────── */
/* Tailwind v4 derives the `z-overlay` utility from the --z-index-* namespace,
so this MUST be --z-index-overlay (a plain --z-overlay generates no class). */
--z-index-overlay: 200; /* z-overlay — modals, dropdowns, popovers */
}Le nom de l'utilitaire reste z-overlay, parce que Tailwind retire le préfixe de namespace du nom du token. Les 22 points d'appel répartis sur 17 composants — Dialog, Select, Tooltip, DropdownMenu, CommandPalette, Lightbox, LandingHeader — étaient déjà corrects et n'ont demandé aucune modification.
Comment l'attraper pour de vrai
Greppez les sources et vous n'apprenez rien : z-overlay est présent dans 17 fichiers, qu'il se résolve ou non. Greppez la sortie de build :
pnpm build
grep -o '\.z-overlay{[^}]*}' .next/static/chunks/*.cssQuand ça marche :
.next/static/chunks/2qvhgub5q_n5s.css:.z-overlay{z-index:200}Cassé : code de sortie 1, aucune sortie. Cette seule commande fait la différence entre « le token est défini » et « la classe existe ». Lancez-la sur n'importe quel utilitaire au nom personnalisé que vous introduisez — ça se généralise à bg-*, rounded-*, tout ce que vous avez tokenisé à la main.
Dernière chose, et c'est pour ça que je n'ai rencontré ça que dans un seul projet. Mes quatre autres templates ne définissent aucun token --z-index-* : trois d'entre eux empilent avec des valeurs arbitraires — z-[90], z-[100], z-[250] — et le quatrième écrit z-index à la main en CSS pur. Les deux voies contournent complètement le thème et compilent toujours. Booking est le seul où j'ai promu les nombres magiques en token nommé, ce qui est la seule façon de déclencher la règle des namespaces. Ça veut aussi dire que copier un composant depuis booking vers l'un des quatre autres perdrait silencieusement son z-index : le nom de la classe voyage, le token non.
C'est le pattern que je livre dans le template booking, démo live sur booking.violettadev.com.
Vérifié sur tailwindcss@4.3.2 avec next@16.2.10.