Un portal renderiza parte de un componente en otro lugar del DOM, normalmente document.body, mientras sigue en el mismo lugar del árbol de React. Lo creas con createPortal(children, domNode) de react-dom. Los portales son la forma en que los modales, tooltips y menús desplegables escapan de un padre que tiene overflow: hidden o su propio contexto de apilamiento.
La caja de abajo recorta todo lo que sobresale. Abre ambos tips.
El tip normal queda cortado en el borde punteado. El tip del portal aparece completo, porque su nodo DOM es hijo de <body>, no de la caja. Borra overflow: 'hidden' de la caja y el tip normal deja de recortarse.
La sintaxis
import { createPortal } from 'react-dom';
createPortal(children, domNode, key?)
childrenes cualquier JSX: un elemento, un fragmento, un componente.domNodees un elemento del DOM existente, comodocument.bodyodocument.getElementById('modal-root'). Debe existir cuando el portal se renderiza.keyes opcional, para cuando renderizas una lista de portales.
createPortal devuelve algo que pones en tu JSX como cualquier elemento. No renderiza nada en la posición del DOM propia del padre.
Un modal en document.body
Un modal es el caso clásico. Dentro de una tarjeta con un transform, un overlay con position: fixed se posiciona respecto a esa tarjeta en lugar de la ventana, y el overflow: hidden de la tarjeta lo recorta. Renderizado en document.body, cubre toda la ventana.
Abre el modal y luego pulsa Escape o haz clic en el overlay oscuro para cerrarlo. Al abrirse, el foco pasa al botón Close. Ahora devuelve el overlay sin createPortal (quita la llamada y su argumento document.body): el overlay se reduce al tamaño de la tarjeta, porque transform convierte la tarjeta en el bloque contenedor de los elementos fijos.
Los eventos se propagan por el árbol de React
Un portal cambia dónde vive el nodo del DOM, no dónde vive el componente. Los eventos de React se propagan hacia arriba por el árbol de React, así que un clic dentro de un portal llega al onClick del componente que lo renderizó, aunque en el DOM el botón sea hijo de <body>.
Los listeners nativos son distintos. Un listener agregado con addEventListener sigue el árbol del DOM y nunca ve el clic.
Haz clic en "Inside the wrapper": registran tanto el manejador de React como el listener nativo. Haz clic en "In a portal": solo registra el manejador de React. El botón del portal está fuera del envoltorio punteado en pantalla, y aun así React lo sigue tratando como hijo.
Normalmente esto es lo que quieres: un onClick en el padre de un menú ve los clics en los elementos de su portal, y los proveedores de contexto que están por encima del componente también se aplican dentro del portal. Puede sorprenderte con la lógica de "clic fuera para cerrar". Un onClick en un envoltorio que cierra un menú también se dispara con los clics dentro del portal del menú, así que detén la propagación dentro del menú (e.stopPropagation(), explicado en la página de eventos), o usa un listener nativo en document y comprueba si el target está dentro del nodo DOM del portal.
Portales en tu propio contenedor
document.body es el destino más simple. Algunas apps agregan un contenedor dedicado a index.html para que todos los overlays compartan un lugar y un orden de apilamiento:
<body>
<div id="root"></div>
<div id="modal-root"></div>
</body>
createPortal(<Modal />, document.getElementById('modal-root'));
Con renderizado en el servidor (Next.js y otros frameworks), document no existe en el servidor. Renderiza el portal solo después de que el componente se haya montado, por ejemplo detrás de un estado mounted que un efecto pone en true.
Cuando el portal tiene que posicionarse junto a su disparador, mide primero el disparador. El primer ejemplo lo mide en el manejador de clic; para un tooltip que no debe parpadear, mide en useLayoutEffect, que se ejecuta antes de que el navegador pinte.
Modales accesibles
Mover el marcado a document.body no hace nada por sí solo para los usuarios de teclado y de lectores de pantalla. Un modal también necesita:
role="dialog"yaria-modal="true", conaria-labelledbyapuntando a su título.- Mover el foco dentro del diálogo al abrirlo (el ejemplo da foco a Close), y devolverlo al botón que lo abrió al cerrarlo.
- Escape para cerrar.
- Mantener el foco dentro mientras está abierto, para que Tab no recorra la página de detrás. Poner el atributo
inerten la raíz de la app mientras el modal está abierto bloquea ahí el foco y los clics.
El elemento nativo <dialog> maneja casi todo esto por ti. Abierto con dialogRef.current.showModal(), se dibuja en la capa superior del navegador por encima de cualquier z-index, deja inerte el resto de la página y se cierra con Escape. No necesita un portal, así que es una buena opción por defecto para diálogos de confirmación simples; los portales siguen siendo la herramienta para tooltips, menús y overlays personalizados.
Por qué z-index no basta
Los desarrolladores suelen recurrir a un portal cuando un z-index grande no consigue poner un menú por encima del resto de la página. La razón son los contextos de apilamiento. Un elemento con position y un z-index, un opacity menor que 1, un transform, un filter o isolation: isolate inicia un nuevo contexto de apilamiento, y los valores de z-index de sus hijos solo compiten entre sí dentro de él. Un hijo con z-index: 9999 dentro de una tarjeta con z-index: 1 sigue quedando debajo de una tarjeta hermana con z-index: 2.
Un portal saca el elemento de todos esos ancestros. Como hijo de <body>, su z-index se compara con los elementos de nivel superior de la página, así que un valor modesto como 1000 basta para los overlays.
Errores comunes
Hacer un portal hacia un nodo que todavía no existe. document.getElementById('modal-root') devuelve null si falta el elemento, y createPortal lanza "Target container is not a DOM element". Revisa el HTML, o haz el portal hacia document.body.
Crear el nodo de destino durante el renderizado. Escribir document.createElement('div') en el cuerpo del componente crea un nodo nuevo en cada renderizado. Créalo una vez en un efecto, o usa un contenedor fijo.
Popups que no siguen a su disparador. Un tooltip posicionado con getBoundingClientRect() no sigue a su disparador cuando la página se desplaza o cambia de tamaño. Vuelve a calcular en scroll y resize (y quita esos listeners en la limpieza del efecto), o cierra el tooltip cuando la página se desplace.
Preguntas frecuentes
¿Qué es un portal en React?
Una forma de renderizar hijos en un nodo del DOM fuera del elemento DOM del componente padre. Lo creas con createPortal(children, domNode) de react-dom. Los hijos conservan su lugar en el árbol de React, así que las props, el estado y el contexto funcionan como siempre.
¿Cuándo debo usar un portal?
Cuando algo debe aparecer encima o fuera de su contenedor: modales, tooltips, menús desplegables, avisos. Un padre con overflow: hidden, un transform o su propio contexto de apilamiento lo recortaría u ocultaría.
¿Los eventos se propagan fuera de un portal?
Sí, por el árbol de React. Un clic dentro de un portal llega a los manejadores onClick de los padres de React, aunque el nodo DOM viva en document.body. Los listeners nativos agregados con addEventListener siguen en cambio el árbol del DOM.
¿El contexto funciona dentro de un portal?
Sí. El contexto, como los eventos, sigue el árbol de React. Un modal renderizado en document.body sigue leyendo el tema o el usuario de los proveedores que están por encima del componente que lo creó.
¿Necesito un portal para un modal?
No siempre. El elemento nativo <dialog> abierto con showModal() se dibuja en la capa superior del navegador, por encima de cualquier z-index, con el manejo del foco integrado. Un portal es la opción habitual para modales personalizados y para tooltips y menús.