Menu

React Portals: createPortal für Modals und Tooltips

createPortal rendert einen Teil einer Komponente in einen anderen DOM-Knoten, etwa document.body, während er an derselben Stelle im React-Baum bleibt. Nutze es für Modals, Tooltips und Menüs, die overflow hidden und der Stapelung per z-index entkommen müssen.

Diese Seite enthält ausführbare Editoren - bearbeiten, ausführen und Ausgabe sofort sehen.

Ein Portal rendert einen Teil einer Komponente an eine andere Stelle im DOM, meist document.body, während er an derselben Stelle im React-Baum bleibt. Du erzeugst eines mit createPortal(children, domNode) aus react-dom. Mit Portals entkommen Modals, Tooltips und Dropdown-Menüs einem Elternteil, das overflow: hidden oder einen eigenen Stapelkontext hat.

Die Box unten schneidet alles ab, was aus ihr herausragt. Öffne beide Tipps.

Der normale Tipp wird am gestrichelten Rand abgeschnitten. Der Portal-Tipp erscheint vollständig, weil sein DOM-Knoten ein Kind von <body> ist, nicht der Box. Lösch overflow: 'hidden' an der Box, und der normale Tipp wird nicht mehr abgeschnitten.

Die Syntax

import { createPortal } from 'react-dom';

createPortal(children, domNode, key?)
  • children ist beliebiges JSX: ein Element, ein Fragment, eine Komponente.
  • domNode ist ein bestehendes DOM-Element, etwa document.body oder document.getElementById('modal-root'). Es muss existieren, wenn das Portal rendert.
  • key ist optional, für den Fall, dass du eine Liste von Portals renderst.

createPortal gibt etwas zurück, das du wie jedes Element in dein JSX setzt. An der eigenen DOM-Position des Elternteils rendert es nichts.

Ein Modal in document.body

Ein Modal ist der klassische Fall. In einer Karte mit transform wird ein Overlay mit position: fixed relativ zu dieser Karte positioniert statt zum Fenster, und das overflow: hidden der Karte schneidet es dann ab. In document.body gerendert bedeckt es den ganzen Viewport.

Öffne das Modal und drück dann Escape oder klicke auf das dunkle Overlay, um es zu schließen. Beim Öffnen springt der Fokus auf den Button Close. Gib das Overlay jetzt ohne createPortal zurück (entferne den Aufruf und sein Argument document.body): Das Overlay schrumpft auf die Größe der Karte, weil transform die Karte zum umgebenden Block für fixierte Elemente macht.

Events steigen durch den React-Baum auf

Ein Portal ändert, wo der DOM-Knoten lebt, nicht wo die Komponente lebt. React-Events steigen den React-Baum hinauf, also erreicht ein Klick in einem Portal das onClick der Komponente, die es gerendert hat, obwohl der Button im DOM ein Kind von <body> ist.

Native Listener sind anders. Ein mit addEventListener hinzugefügter Listener folgt dem DOM-Baum und sieht den Klick nie.

Klicke auf „Inside the wrapper“: Sowohl der React-Handler als auch der native Listener loggen. Klicke auf „In a portal“: Nur der React-Handler loggt. Der Portal-Button liegt auf dem Bildschirm außerhalb des gestrichelten Wrappers, und doch behandelt React ihn weiterhin als Kind.

Meist willst du genau das: Ein onClick am Elternteil eines Menüs sieht Klicks auf seine Portal-Einträge, und Context-Provider über der Komponente gelten auch im Portal. Bei einer Logik „Klick außerhalb schließt“ kann das überraschen. Ein onClick an einem Wrapper, das ein Menü schließt, feuert auch bei Klicks im Portal des Menüs, also stoppe die Ausbreitung im Menü (e.stopPropagation(), behandelt auf der Seite zu Events) oder nutze einen nativen Listener auf document und prüfe, ob das Ziel im DOM-Knoten des Portals liegt.

Portals in deinen eigenen Container

document.body ist das einfachste Ziel. Manche Apps fügen index.html einen eigenen Container hinzu, damit alle Overlays einen gemeinsamen Ort und eine gemeinsame Stapelreihenfolge haben:

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

Beim Server-Rendering (Next.js und andere Frameworks) existiert document auf dem Server nicht. Rendere das Portal erst, nachdem die Komponente gemountet ist, zum Beispiel hinter einem State mounted, den ein Effekt auf true setzt.

Wenn das Portal neben seinem Auslöser positioniert werden muss, miss zuerst den Auslöser. Das erste Beispiel misst ihn im Klick-Handler; für einen Tooltip, der nicht flackern darf, miss in useLayoutEffect, der läuft, bevor der Browser zeichnet.

Barrierefreie Modals

Das Markup nach document.body zu verschieben bringt allein für Tastatur- und Screenreader-Nutzer nichts. Ein Modal braucht außerdem:

  • role="dialog" und aria-modal="true", mit aria-labelledby, das auf seinen Titel zeigt.
  • Den Fokus, der beim Öffnen in den Dialog wandert (das Beispiel fokussiert Close) und beim Schließen zurück zum Button, der ihn geöffnet hat.
  • Escape zum Schließen.
  • Den Fokus, der darin bleibt, solange er offen ist, damit Tab nicht in die Seite dahinter wandert. Das Attribut inert am App-Root, solange das Modal offen ist, blockiert dort Fokus und Klicks.

Das native Element <dialog> erledigt das meiste davon für dich. Mit dialogRef.current.showModal() geöffnet wird es in der Top Layer des Browsers über jedem z-index gezeichnet, macht den Rest der Seite inert und schließt sich bei Escape. Es braucht kein Portal, also ist es ein guter Standard für einfache Bestätigungsdialoge; Portals bleiben das Werkzeug für Tooltips, Menüs und eigene Overlays.

Warum z-index allein nicht reicht

Entwickler greifen oft zu einem Portal, nachdem ein großer z-index ein Menü nicht über den Rest der Seite bringen konnte. Der Grund sind Stapelkontexte. Ein Element mit position und einem z-index, einer opacity unter 1, einem transform, einem filter oder isolation: isolate startet einen neuen Stapelkontext, und die z-index-Werte seiner Kinder konkurrieren nur untereinander darin. Ein Kind mit z-index: 9999 in einer Karte mit z-index: 1 liegt trotzdem unter einer Geschwisterkarte mit z-index: 2.

Ein Portal holt das Element aus jedem solchen Vorfahren heraus. Als Kind von <body> wird sein z-index mit den Elementen auf oberster Ebene der Seite verglichen, also reicht für Overlays ein bescheidener Wert wie 1000.

Typische Fehler

In einen Knoten portieren, der noch nicht existiert. document.getElementById('modal-root') gibt null zurück, wenn das Element fehlt, und createPortal wirft „Target container is not a DOM element“. Prüfe das HTML oder portiere in document.body.

Den Zielknoten beim Rendern erzeugen. document.createElement('div') im Komponentenkörper erzeugt bei jedem Render einen neuen Knoten. Erzeuge ihn einmal in einem Effekt oder nutze einen festen Container.

Popups, die ihrem Auslöser nicht folgen. Ein Tooltip, der mit getBoundingClientRect() positioniert wird, folgt seinem Auslöser nicht, wenn die Seite scrollt oder ihre Größe ändert. Berechne bei scroll und resize neu (und entferne diese Listener im Cleanup des Effekts) oder schließ den Tooltip, wenn die Seite scrollt.

Häufig gestellte Fragen

Was ist ein Portal in React?

Ein Weg, Kinder in einen DOM-Knoten außerhalb des DOM-Elements der Elternkomponente zu rendern. Du erzeugst eines mit createPortal(children, domNode) aus react-dom. Die Kinder behalten ihren Platz im React-Baum, also funktionieren Props, State und Context wie gewohnt.

Wann sollte ich ein Portal nutzen?

Wenn etwas über oder außerhalb seines Containers erscheinen muss: Modals, Tooltips, Dropdown-Menüs, Toasts. Ein Elternteil mit overflow: hidden, einem transform oder einem eigenen Stapelkontext würde es sonst abschneiden oder verdecken.

Steigen Events aus einem Portal auf?

Ja, durch den React-Baum. Ein Klick in einem Portal erreicht die onClick-Handler der React-Eltern, obwohl der DOM-Knoten in document.body lebt. Native Listener, die mit addEventListener hinzugefügt wurden, folgen dagegen dem DOM-Baum.

Funktioniert Context in einem Portal?

Ja. Context folgt wie Events dem React-Baum. Ein Modal, das in document.body gerendert wird, liest weiterhin das Theme oder den Nutzer aus Providern über der Komponente, die es erzeugt hat.

Brauche ich für ein Modal ein Portal?

Nicht immer. Das native Element <dialog>, geöffnet mit showModal(), wird in der Top Layer des Browsers gezeichnet, über jedem z-index, mit eingebauter Fokusverwaltung. Ein Portal ist die übliche Wahl für eigene Modals sowie für Tooltips und Menüs.

Illustration der Programmiersprachen bei Coddy

Lerne mit Coddy zu programmieren

LOS GEHT'S