Menu

Portails React : createPortal pour modales et infobulles

createPortal affiche une partie d'un composant dans un autre nœud DOM, comme document.body, tout en la gardant à la même place dans l'arbre React. Utilisez-le pour les modales, les infobulles et les menus qui doivent échapper à overflow hidden et à l'empilement des z-index.

Cette page contient des éditeurs exécutables - modifiez, exécutez et voyez la sortie instantanément.

Un portail affiche une partie d'un composant à un autre endroit du DOM, généralement document.body, tout en la gardant à la même place dans l'arbre React. Vous en créez un avec createPortal(children, domNode) depuis react-dom. C'est grâce aux portails que les modales, les infobulles et les menus déroulants échappent à un parent qui a overflow: hidden ou son propre contexte d'empilement.

La boîte ci-dessous coupe tout ce qui dépasse. Ouvrez les deux astuces.

L'astuce normale est coupée à la bordure en pointillés. L'astuce en portail apparaît en entier, car son nœud DOM est un enfant de <body>, pas de la boîte. Supprimez overflow: 'hidden' de la boîte et l'astuce normale n'est plus coupée.

La syntaxe

import { createPortal } from 'react-dom';

createPortal(children, domNode, key?)
  • children est n'importe quel JSX : un élément, un fragment, un composant.
  • domNode est un élément DOM existant, comme document.body ou document.getElementById('modal-root'). Il doit exister au moment du rendu du portail.
  • key est facultatif, pour le cas où vous affichez une liste de portails.

createPortal renvoie quelque chose que vous placez dans votre JSX comme n'importe quel élément. Il n'affiche rien à la position DOM du parent lui-même.

Une modale dans document.body

Une modale est le cas classique. Dans une carte qui a un transform, un overlay en position: fixed est positionné par rapport à cette carte au lieu de la fenêtre, et le overflow: hidden de la carte le coupe ensuite. Affiché dans document.body, il couvre toute la fenêtre.

Ouvrez la modale, puis appuyez sur Échap ou cliquez sur l'overlay sombre pour la fermer. Le focus passe au bouton Close à l'ouverture. Renvoyez maintenant l'overlay sans createPortal (retirez l'appel et son argument document.body) : l'overlay se réduit à la taille de la carte, car transform fait de la carte le bloc conteneur des éléments fixes.

Les événements remontent à travers l'arbre React

Un portail change l'endroit où vit le nœud DOM, pas celui où vit le composant. Les événements React remontent l'arbre React, donc un clic dans un portail atteint le onClick du composant qui l'a affiché, même si dans le DOM le bouton est un enfant de <body>.

Les écouteurs natifs sont différents. Un écouteur ajouté avec addEventListener suit l'arbre DOM et ne voit jamais le clic.

Cliquez sur "Inside the wrapper" : le gestionnaire React et l'écouteur natif écrivent tous les deux. Cliquez sur "In a portal" : seul le gestionnaire React écrit. Le bouton en portail se trouve à l'écran hors de l'enveloppe en pointillés, et pourtant React le traite toujours comme un enfant.

C'est généralement ce que vous voulez : un onClick sur le parent d'un menu voit les clics sur les éléments en portail du menu, et les providers de contexte au-dessus du composant s'appliquent aussi dans le portail. Cela peut surprendre avec une logique « cliquer à l'extérieur pour fermer ». Un onClick sur une enveloppe qui ferme un menu se déclenche aussi pour les clics à l'intérieur du portail du menu : stoppez donc la propagation dans le menu (e.stopPropagation(), traité sur la page des événements), ou utilisez un écouteur natif sur document et vérifiez si la cible se trouve dans le nœud DOM du portail.

Des portails dans votre propre conteneur

document.body est la cible la plus simple. Certaines applications ajoutent un conteneur dédié dans index.html pour que tous les overlays partagent un même endroit et un même ordre d'empilement :

<body>
    <div id="root"></div>
    <div id="modal-root"></div>
</body>
createPortal(<Modal />, document.getElementById('modal-root'));

Avec le rendu serveur (Next.js et d'autres frameworks), document n'existe pas sur le serveur. N'affichez le portail qu'après le montage du composant, par exemple derrière un état mounted qu'un effet passe à true.

Quand le portail doit être positionné à côté de son déclencheur, mesurez d'abord le déclencheur. Le premier exemple le mesure dans le gestionnaire de clic ; pour une infobulle qui ne doit pas scintiller, mesurez dans useLayoutEffect, qui s'exécute avant que le navigateur affiche la page.

Des modales accessibles

Déplacer le balisage dans document.body ne fait rien en soi pour les utilisateurs de clavier et de lecteurs d'écran. Une modale a aussi besoin de :

  • role="dialog" et aria-modal="true", avec aria-labelledby qui désigne son titre.
  • Le focus déplacé dans la boîte de dialogue à l'ouverture (l'exemple donne le focus à Close), puis rendu au bouton qui l'a ouverte à la fermeture.
  • Échap pour fermer.
  • Le focus maintenu à l'intérieur pendant qu'elle est ouverte, pour que Tab ne parcoure pas la page en arrière-plan. Placer l'attribut inert sur la racine de l'application pendant que la modale est ouverte y bloque le focus et les clics.

L'élément natif <dialog> gère l'essentiel de cela pour vous. Ouvert avec dialogRef.current.showModal(), il est dessiné dans la couche supérieure du navigateur au-dessus de tout z-index, rend le reste de la page inerte et se ferme avec Échap. Il n'a pas besoin de portail, c'est donc un bon choix par défaut pour de simples boîtes de confirmation ; les portails restent l'outil des infobulles, des menus et des overlays personnalisés.

Pourquoi z-index seul ne suffit pas

Les développeurs se tournent souvent vers un portail après qu'un gros z-index n'a pas réussi à placer un menu au-dessus du reste de la page. La raison, ce sont les contextes d'empilement. Un élément avec position et un z-index, une opacity inférieure à 1, un transform, un filter ou isolation: isolate démarre un nouveau contexte d'empilement, et les valeurs z-index de ses enfants ne sont comparées qu'entre elles à l'intérieur. Un enfant avec z-index: 9999 dans une carte avec z-index: 1 reste sous une carte sœur avec z-index: 2.

Un portail sort l'élément de tous ces ancêtres. En tant qu'enfant de <body>, son z-index est comparé aux éléments de premier niveau de la page, donc une valeur modeste comme 1000 suffit pour les overlays.

Erreurs courantes

Viser avec un portail un nœud qui n'existe pas encore. document.getElementById('modal-root') renvoie null si l'élément manque, et createPortal lève "Target container is not a DOM element". Vérifiez le HTML, ou utilisez document.body comme cible.

Créer le nœud cible pendant le rendu. Écrire document.createElement('div') dans le corps du composant crée un nouveau nœud à chaque rendu. Créez-le une fois dans un effet, ou utilisez un conteneur fixe.

Des popups qui ne suivent pas leur déclencheur. Une infobulle positionnée à partir de getBoundingClientRect() ne suit pas son déclencheur quand la page défile ou est redimensionnée. Recalculez la position sur scroll et resize (et retirez ces écouteurs dans le nettoyage de l'effet), ou fermez l'infobulle quand la page défile.

Questions fréquentes

Qu'est-ce qu'un portail dans React ?

Un moyen d'afficher des enfants dans un nœud DOM situé hors de l'élément DOM du composant parent. Vous en créez un avec createPortal(children, domNode) depuis react-dom. Les enfants gardent leur place dans l'arbre React, donc les props, l'état et le contexte fonctionnent normalement.

Quand utiliser un portail ?

Quand quelque chose doit apparaître au-dessus de son conteneur ou en dehors : modales, infobulles, menus déroulants, notifications. Sinon, un parent avec overflow: hidden, un transform ou son propre contexte d'empilement le couperait ou le masquerait.

Les événements remontent-ils hors d'un portail ?

Oui, à travers l'arbre React. Un clic dans un portail atteint les gestionnaires onClick des parents React, même si le nœud DOM vit dans document.body. Les écouteurs natifs ajoutés avec addEventListener suivent plutôt l'arbre DOM.

Le contexte fonctionne-t-il dans un portail ?

Oui. Le contexte, comme les événements, suit l'arbre React. Une modale affichée dans document.body lit toujours le thème ou l'utilisateur fournis par les providers au-dessus du composant qui l'a créée.

Faut-il un portail pour une modale ?

Pas toujours. L'élément natif <dialog> ouvert avec showModal() est dessiné dans la couche supérieure du navigateur, au-dessus de tout z-index, avec une gestion du focus intégrée. Un portail est le choix habituel pour les modales personnalisées, les infobulles et les menus.

Illustration des langages de programmation de Coddy

Apprendre à coder avec Coddy

COMMENCER