Menu

Portal in React: createPortal per modali e tooltip

createPortal renderizza una parte di un componente in un nodo DOM diverso, come document.body, mentre resta nella stessa posizione dell'albero React. Usalo per modali, tooltip e menu che devono sfuggire a overflow hidden e all'impilamento dello z-index.

Questa pagina include editor eseguibili: modifica, esegui e vedi subito l'output.

Un portal renderizza una parte di un componente in un punto diverso del DOM, di solito document.body, mentre resta nella stessa posizione dell'albero React. Ne crei uno con createPortal(children, domNode) da react-dom. I portal sono il modo in cui modali, tooltip e menu a tendina sfuggono a un genitore che ha overflow: hidden o un proprio contesto di impilamento.

Il riquadro qui sotto taglia tutto ciò che sporge. Apri entrambi i suggerimenti.

Il suggerimento normale viene tagliato al bordo tratteggiato. Quello con il portal compare per intero, perché il suo nodo DOM è figlio di <body>, non del riquadro. Elimina overflow: 'hidden' dal riquadro e il suggerimento normale non viene più tagliato.

La sintassi

import { createPortal } from 'react-dom';

createPortal(children, domNode, key?)
  • children è qualsiasi JSX: un elemento, un fragment, un componente.
  • domNode è un elemento DOM esistente, come document.body o document.getElementById('modal-root'). Deve esistere quando il portal viene renderizzato.
  • key è facoltativa, per quando renderizzi una lista di portal.

createPortal restituisce qualcosa che metti nel tuo JSX come qualsiasi elemento. Non renderizza nulla nella posizione DOM del genitore.

Una modale in document.body

Una modale è il caso classico. Dentro una card con un transform, un overlay con position: fixed viene posizionato rispetto a quella card invece che alla finestra, e l'overflow: hidden della card lo taglia. Renderizzato in document.body, copre l'intera area visibile.

Apri la modale, poi premi Esc o clicca l'overlay scuro per chiuderla. Quando si apre, il focus passa al pulsante Close. Ora restituisci l'overlay senza createPortal (togli la chiamata e il suo argomento document.body): l'overlay si riduce alle dimensioni della card, perché transform rende la card il blocco contenitore per gli elementi fixed.

Gli eventi risalgono attraverso l'albero React

Un portal cambia dove vive il nodo DOM, non dove vive il componente. Gli eventi React risalgono l'albero React, quindi un clic dentro un portal raggiunge l'onClick del componente che lo ha renderizzato, anche se nel DOM il pulsante è figlio di <body>.

I listener nativi sono diversi. Un listener aggiunto con addEventListener segue l'albero DOM e non vede mai il clic.

Clicca "Inside the wrapper": registrano sia il gestore React sia il listener nativo. Clicca "In a portal": registra solo il gestore React. Sullo schermo il pulsante del portal sta fuori dal wrapper tratteggiato, eppure React lo tratta comunque come figlio.

Di solito è ciò che vuoi: un onClick sul genitore di un menu vede i clic sulle voci del suo portal, e i provider di context sopra il componente valgono anche dentro il portal. Può sorprenderti con la logica "clicca fuori per chiudere". Un onClick su un wrapper che chiude un menu scatta anche per i clic dentro il portal del menu, quindi ferma la propagazione dentro il menu (e.stopPropagation(), trattato nella pagina sugli eventi), oppure usa un listener nativo su document e verifica se il target sta dentro il nodo DOM del portal.

Portal nel tuo contenitore

document.body è la destinazione più semplice. Alcune app aggiungono a index.html un contenitore dedicato così tutti gli overlay condividono un solo posto e un solo ordine di impilamento:

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

Con il rendering lato server (Next.js e altri framework), document non esiste sul server. Renderizza il portal solo dopo che il componente è stato montato, per esempio dietro uno stato mounted che un effetto imposta a true.

Quando il portal deve essere posizionato accanto al suo attivatore, prima misura l'attivatore. Il primo esempio lo misura nel gestore del clic; per un tooltip che non deve sfarfallare, misura in useLayoutEffect, che viene eseguito prima che il browser disegni la pagina.

Modali accessibili

Spostare il markup in document.body di per sé non fa nulla per chi usa la tastiera o uno screen reader. Una modale ha bisogno anche di:

  • role="dialog" e aria-modal="true", con aria-labelledby che punta al suo titolo.
  • Il focus spostato dentro la finestra quando si apre (l'esempio dà il focus a Close), e riportato al pulsante che l'ha aperta quando si chiude.
  • Esc per chiudere.
  • Il focus mantenuto all'interno mentre è aperta, così Tab non va a finire nella pagina dietro. Impostare l'attributo inert sulla root dell'app mentre la modale è aperta blocca lì focus e clic.

L'elemento nativo <dialog> gestisce per te la maggior parte di queste cose. Aperto con dialogRef.current.showModal(), viene disegnato nel top layer del browser sopra qualsiasi z-index, rende inerte il resto della pagina e si chiude con Esc. Non ha bisogno di un portal, quindi è una buona scelta predefinita per semplici finestre di conferma; i portal restano lo strumento per tooltip, menu e overlay personalizzati.

Perché lo z-index da solo non basta

Spesso gli sviluppatori ricorrono a un portal dopo che uno z-index grande non è riuscito a mettere un menu sopra il resto della pagina. Il motivo sono i contesti di impilamento. Un elemento con position e uno z-index, un'opacity sotto 1, un transform, un filter o isolation: isolate avvia un nuovo contesto di impilamento, e i valori di z-index dei suoi figli competono solo tra loro al suo interno. Un figlio con z-index: 9999 dentro una card con z-index: 1 sta comunque sotto una card sorella con z-index: 2.

Un portal sposta l'elemento fuori da ogni antenato di questo tipo. Come figlio di <body>, il suo z-index viene confrontato con gli elementi di primo livello della pagina, quindi per gli overlay basta un valore modesto come 1000.

Errori comuni

Fare un portal in un nodo che non esiste ancora. document.getElementById('modal-root') restituisce null se l'elemento manca, e createPortal genera "Target container is not a DOM element". Controlla l'HTML, oppure fai il portal in document.body.

Creare il nodo di destinazione durante il rendering. Scrivere document.createElement('div') nel corpo del componente crea un nodo nuovo a ogni rendering. Crealo una volta in un effetto, oppure usa un contenitore fisso.

Popup che non seguono il loro attivatore. Un tooltip posizionato con getBoundingClientRect() non segue il suo attivatore quando la pagina scorre o si ridimensiona. Ricalcola su scroll e resize (e rimuovi quei listener nella funzione di cleanup dell'effetto), oppure chiudi il tooltip quando la pagina scorre.

Domande frequenti

Cos'è un portal in React?

Un modo per renderizzare dei figli in un nodo DOM al di fuori dell'elemento DOM del componente genitore. Ne crei uno con createPortal(children, domNode) da react-dom. I figli mantengono il loro posto nell'albero React, quindi props, stato e context funzionano come al solito.

Quando dovrei usare un portal?

Quando qualcosa deve comparire sopra o fuori dal suo contenitore: modali, tooltip, menu a tendina, notifiche. Un genitore con overflow: hidden, un transform o un proprio contesto di impilamento altrimenti lo taglierebbe o lo nasconderebbe.

Gli eventi risalgono fuori da un portal?

Sì, attraverso l'albero React. Un clic dentro un portal raggiunge i gestori onClick dei genitori React, anche se il nodo DOM vive in document.body. I listener nativi aggiunti con addEventListener seguono invece l'albero DOM.

Il context funziona dentro un portal?

Sì. Il context, come gli eventi, segue l'albero React. Una modale renderizzata in document.body legge comunque il tema o l'utente dai provider sopra il componente che l'ha creata.

Mi serve un portal per una modale?

Non sempre. L'elemento nativo <dialog> aperto con showModal() viene disegnato nel top layer del browser, sopra qualsiasi z-index, con la gestione del focus integrata. Un portal è la scelta abituale per le modali personalizzate e per tooltip e menu.

Illustrazione dei linguaggi di programmazione di Coddy

Impara a programmare con Coddy

INIZIA