React

Portal per modali e tooltip, senza trappole

createPortal sposta il nodo nel body e lascia eventi e contesto nell'albero React: la modale esce dal CSS, non dai componenti.

In sintesi

  • createPortal cambia il nodo DOM di destinazione, non la posizione nell'albero React.
  • Un evento dentro il portal continua a risalire i componenti genitori, non i nodi del body.
  • Il focus va spostato dentro la modale all'apertura e restituito al trigger alla chiusura.

Un portal rende un figlio in un nodo DOM diverso, di solito document.body, e lascia intatti contesto, stato ed eventi React. Serve a far uscire modali e tooltip da un contenitore con overflow nascosto o con uno z-index che li taglia.

Cosa resta e cosa se ne va

Il nodo si sposta nel body; gli eventi bolle seguono l'albero React, non il DOM. Un click dentro la modale raggiunge ancora l'handler del componente genitore, e questo sorprende chi si aspetta l'isolamento.

Il caso eseguibile

import { createPortal } from "react-dom";
import { useEffect, useRef } from "react";

export function Modale({ aperta, onChiudi, children }) {
  const ritorno = useRef(null);

  useEffect(() => {
    if (!aperta) return;
    ritorno.current = document.activeElement;
    const onKey = (e) => e.key === "Escape" && onChiudi();
    document.addEventListener("keydown", onKey);
    return () => {
      document.removeEventListener("keydown", onKey);
      ritorno.current?.focus();
    };
  }, [aperta, onChiudi]);

  if (!aperta) return null;

  return createPortal(
    
      {children}
    ,
    document.body,
  );
}

Il codice sposta il contenuto nel body, chiude con Escape e riporta il focus sull'elemento che ha aperto la modale. La trappola del focus non è inclusa: per quella conviene una primitiva Radix o React Aria, entrambe con licenza MIT.

Tooltip: portal o no

Un tooltip dentro un contenitore con overflow nascosto viene tagliato, e il portal lo salva. Il prezzo è il posizionamento: le coordinate vanno calcolate rispetto al viewport e aggiornate allo scroll, un lavoro che Floating UI, licenza MIT, fa già.

La categoria dialog raccoglie le modali del catalogo, con il dialog di Origin UI su primitive Radix; per le etichette brevi c'è la categoria tooltip.

La limitazione

Il portal non porta con sé il CSS: uno stile scritto sotto un contenitore smette di applicarsi nel body, e le variabili CSS definite su un wrapper non arrivano al nodo spostato. I token vanno sul selettore radice, non su un div intermedio.

Dove continuare

La checklist dei componenti accessibili è in componenti React accessibili; le primitive sono in Radix, Base UI e React Aria; il vocabolario ARIA è in che cos'è ARIA. Il confronto fra React e Svelte sui siti di contenuto è in Svelte vs React.

Domande frequenti

Un portal rompe il contesto React?

No. Il contesto segue l'albero dei componenti, quindi un valore del provider resta leggibile dentro il portal anche se il nodo sta nel body.

Perché la mia modale non prende gli stili?

Perché il nodo vive nel body e non sotto il contenitore che definiva gli stili. Sposta i token sul selettore radice o aggiungi una classe al nodo del portal.

Il click fuori dalla modale chiude da solo?

No. Serve un listener sul documento che confronti il bersaglio con il nodo della modale, e va rimosso alla chiusura.

Meglio portal o dialog nativo?

Il dialog nativo gestisce focus, Escape e livello superiore senza JavaScript. Il portal serve quando ti servono animazioni o un controllo più fine del markup.

Posso usare un portal per un menu a tendina?

Sì, ma il menu deve restare raggiungibile da tastiera e chiudersi con Escape. Un menu in portal senza gestione del focus è peggio di un menu ritagliato.