Un z-index à 10000 sur un drawer suffit à vider tous les menus Select MUI qu’il contient : les options sont bien dans le DOM, mais le menu, à z-index: 1300 par défaut, s’ouvre derrière le panneau blanc opaque. Dans le SaaS d’abonnements sur lequel je travaille (React 18.2, @mui/material 5.14.17), le symptôme remonté était double : des listes déroulantes sans aucune option, puis un écran où plus rien n’était cliquable jusqu’au rechargement. La cause : un commit qui avait monté le drawer de z-index: 2 à 10000 pour faire passer des boutons au-dessus d’un overlay.
Le symptôme : un Select vide, puis une UI figée
Dans le tunnel d’onboarding, on ouvre le drawer d’édition d’un compte marchand, on clique sur la liste « Forme juridique » : le menu s’ouvre, vide. Après ce clic, les champs texte du drawer ne prennent plus le focus.
Le « drawer » est un composant maison, ModalRight : un Modal MUI stylé en panneau latéral par une classe CSS.
Premier réflexe : vérifier les données. Les 5 options figuraient bien dans le DOM. Mais document.elementFromPoint() aux coordonnées de l’une d’elles renvoyait… un champ du drawer. Les options existaient, elles étaient simplement dessous.
La fausse piste : la fonctionnalité qui avait rendu le bug visible
Le ticket avait été attribué à une évolution récente qui affichait la liste « Forme juridique » sur toutes les fiches, et non plus pour certains types de comptes seulement. Elle en avait surtout fait le premier Select manipulé de l’onboarding. Le vrai coupable se trouvait dans l’historique du CSS du drawer :
.modal__right {
position: absolute;
background-color: rgba(0, 0, 0, 0.4);
height: 100%;
- z-index: 2;
+ z-index: 10000;
}
Pourquoi un z-index trop haut vide le menu Select MUI ?
Le menu d’un Select MUI est un Popover, donc un Modal, rendu dans un portal en fin de <body>. Le drawer aussi. Les deux sont frères dans le même contexte d’empilement, et l’échelle MUI est fixée dans @mui/material/styles/zIndex.js :
| Élément | z-index |
|---|---|
drawer MUI | 1200 |
modal MUI (dont Menu, Popover, Select) | 1300 |
snackbar MUI | 1400 |
tooltip MUI | 1500 |
| Drawer maison après le commit | 10000 |
À 10000, le drawer passe au-dessus de tous les overlays MUI. Avant, la classe CSS écrasait déjà le 1300 natif du Modal, mais avec 2 : les menus passaient largement devant.
L’UI figée vient du même mécanisme. À l’ouverture, un Modal MUI pose aria-hidden="true" sur ses frères de <body> (le ModalManager de @mui/base) et active un focus-trap. Le menu, invisible, garde le focus ; son backdrop transparent, qui ferme le menu au clic, est lui aussi sous le drawer. Les clics tombent sur le drawer rendu inerte, et le menu ne se ferme jamais.
Un overlay caché derrière un autre n’est pas seulement invisible : s’il pose un focus-trap, il rend inerte tout ce qui le recouvre.
La rustine : surenchérir à 10001
Premier correctif, ciblé, dans le composant partagé SelectWithLabel :
<MuiSelect
{...register(fieldName, { ...requiredProps })}
// Le menu doit passer au-dessus des drawers (z-index 10000)
MenuProps={{ style: { zIndex: 10001 } }}
/>
Le style inline permettait un test simple : expect(popoverRoot.style.zIndex).toBe('10001'). Un fichier _popper.css, présent depuis 2024 comme le z-index: 10000 de l’autre drawer maison (ModalCenter), forçait déjà les date pickers à 10001. Les 29 tests des formulaires concernés passaient.
Peu après, le même symptôme revenait sur un Select brut, hors SelectWithLabel, dans le drawer « Nouveau document ». Corriger champ par champ oubliait forcément des overlays.
Le vrai correctif : revenir à l’échelle MUI
Plutôt que de surenchérir sur chaque overlay, j’ai redescendu les drawers au niveau modal de MUI, via une variable CSS :
:root {
/* Échelle alignée sur MUI (drawer 1200 / modal 1300 / snackbar 1400 / tooltip 1500) */
--z-index-modal: 1300;
}
.modal__right,
.modal__center {
z-index: var(--z-index-modal);
}
À z-index égal, l’ordre du DOM tranche : le menu, portalé après le drawer, s’affiche au-dessus. C’est le comportement standard d’un Dialog MUI qui contient un Select. Le hack MenuProps et _popper.css ont été supprimés, et le test inverse a été verrouillé : expect(popoverRoot.style.zIndex).toBe(''), pour empêcher la réintroduction d’une surcharge.
Vérification : 82 tests Jest verts sur les composants concernés, puis contrôle dans l’application avec elementFromPoint sur quatre types de champs dans des drawers (Select brut, SelectWithLabel, DatePicker, Autocomplete). Effet de bord bienvenu : les Snackbar et Tooltip MUI natifs, à 1400 et 1500, cachés derrière le 10000, repassaient au-dessus des drawers.
Même famille de bug sur une carte Leaflet : un edge marker caché sous les contrôles, présent mais intappable.
Lessons learned
- Avec MUI, un z-index hors de l’échelle du thème casse tous les overlays d’un coup. Menus, popovers, pickers, Autocomplete sont à 1300 : un conteneur à 10000 les cache tous.
- Des options « vides » dans le DOM signalent un problème d’empilement, pas de données.
document.elementFromPoint()dit en une ligne quel élément reçoit vraiment le clic. - Un Modal MUI caché fige l’interface. Son
aria-hiddenet son focus-trap restent actifs même quand personne ne le voit. - La fonctionnalité qui rend un bug visible n’est pas forcément celle qui l’a introduit. Remonter l’historique des fichiers CSS touchés a désigné le bon commit.
- Surenchérir sur un z-index corrige un champ, s’aligner sur l’échelle les corrige tous. À z-index égal, l’ordre des portals suffit à mettre le menu devant.