Ersetzen von Tooltip-Bibliotheken durch die Popover-API und CSS-Ankerpositionierung
Warum Tooltip-Elemente Popper und eine schwebende Benutzeroberfläche benötigen, wobei ein natives Feature nun das Stapeln, Positionieren und Schließen löst – und wann eine JavaScript-Bibliothek trotzdem sinnvoll ist.
Das Anzeigen einer Textzeile neben einem Button scheint trivial zu sein, doch es existieren Popper.js, Floating UI sowie zahlreiche Wrapper-Pakete – weil das bisher nie möglich war. Ein Tooltip stellt eigentlich drei getrennte Probleme dar, die übereinander geschichtet sind, und bis vor Kurzem bot die Plattform für keines davon eine deklarative Lösung. Diese Anleitung zerlegt diese Probleme, zeigt auf, welchen Aufwand jede davon im veröffentlichten JavaScript bedeutet, und ordnet sie den beiden Browser-Funktionen zu, die nun die gängigen Fälle abdecken: der Popover-API und dem CSS-Ankerpositionieren. Am Ende werden Sie wissen, welche Teile einer Tooltip-Bibliothek Sie löschen können und welche Sie möglicherweise noch benötigen.
Drei Probleme, die sich in einem Tooltip verbergen
Jedes dieser Probleme ist eine seit langem bestehende, gut verstandene Einschränkung im Verhalten des Browsers vor Einführung der neuen Spezifikationen:
- Überlagerung. Wird die Hilfetextanzeige über allem anderen angezeigt, oder wird sie durch
overflow: hiddeneines Vorfahren abgeschnitten? - Positionierung. Weiß die Hilfetextanzeige, wo ihr Auslöser auf dem Bildschirm liegt, und folgt sie dieser Position beim Scrollen und Vergrößern/Verkleinern?
- Abschalten. Schließt sie sich automatisch, wenn der Benutzer an einer anderen Stelle klickt oder die Taste Escape drückt?
Während des größten Teils der Web-Geschichte hat jedes Projekt, das eine Hilfetextanzeige, ein Dropdown-Menü oder eine Autocomplete-Liste benötigte, alle drei Probleme in JavaScript gelöst. Die Lösungen unterscheiden sich je nach Fall, und genau dadurch, dass man sie als ein einziges Problem betrachtet hat, entwickelte sich eine kleine UI-Einzelheit zu einer Abhängigkeit. Deshalb lohnt es sich, sie nacheinander zu betrachten.
Überlagerung: Warum z-index seinen Kontext nicht verlassen kann
Jedes Element gehört zu einem Stapelkontext, der entscheidet, was über was dargestellt wird. z-index ordnet die Elemente innerhalb eines einzigen Stapelkontexts; er kann ein Element nicht aus dem Kontext herausheben, zu dem es gehört.
Platzieren Sie die Hilfetextanzeige in einem Container mit overflow: hidden oder innerhalb eines Modals, das seinen eigenen Stapelkontext erstellt – dann wird kein Wert, nicht einmal 999999, dazu führen, dass sie über diese Grenze hinaus sichtbar wird. Die Hilfetextanzeige ist durch die Darstellungsregeln ihrer Vorfahren eingeschränkt, und z-index hat keine Wirkung außerhalb davon.
Das traditionelle Ausstiegsloch war ein Portal: Die Markup-Struktur der Tooltip wird an einer anderen Stelle im Dokument platziert, in der Regel am Ende von <body>, sodass sie nicht mehr die Beschränkungen und Stapelungseigenschaften des übergeordneten Elements erbt. Reacts createPortal existiert größtenteils aus diesem Grund. Es handelt sich dabei weniger um eine React-Funktion als vielmehr um einen Workaround für etwas, was CSS nicht ausdrücken konnte.
Positionierung: Absolute Positionierung versteht nur Vorfahren
position: absolute platziert ein Element relativ zu seinem nächstgelegenen positionierten Vorfahren – also dem nächsten Element in der DOM-Struktur, dessen position auf relative, absolute, fixed oder sticky gesetzt ist. Das Schlüsselwort hier ist Vorfahre: Die Referenz muss sich im selben Zweig des DOM befinden, irgendwo über der Tooltip.
Sobald das Tooltip und sein Auslöser Geschwister sind oder das Tooltip in den <body>-Element gebracht wurde, um Probleme mit der Überlagerung zu lösen, ist der Auslöser nicht mehr ein Vorfahre. CSS hatte keine Möglichkeit, anzugeben „Positioniere dieses Element relativ zu jenem unverwandten Element dort drüben.“ Das Problem lag nicht im Positionieren per CSS im Allgemeinen, sondern am Fehlen einer Positionierungsbeziehung, die die Dokumentstruktur ignoriert.
Bibliotheken füllten diese Lücke durch Messungen. Sie rufen getBoundingClientRect() auf, um den relativ zum Ansichtsfenster des Auslösers definierten Rahmen zu erhalten, berechnen anschließend die Koordinaten für das Tooltip und wiederholen diese Berechnung bei jedem Scrollen oder Resize, da sich die Werte ständig ändern. Dieser kontinuierlich laufende Zyklus aus Messen und Platzieren ist im Wesentlichen das, was eine Positionierungsbibliothek zur Laufzeit tut.
Abschalten: Verhalten, das durch Markup nicht beschrieben werden konnte
Vor der Popover-API hatten HTML und CSS keine Möglichkeit, „beim Klicken außerhalb oder Drücken der Escape-Taste zu schließen“. Dies musste vollständig per Skript umgesetzt werden: Ein Klick-Listener auf document, der prüfte, ob das Ereignistarget außerhalb des Tooltips lag, ein keydown-Listener, der auf die Escape-Taste wartete, sowie Aufräumarbeiten für beide Fälle, wenn das Komponente deinstalliert wurde, damit keine Ressourcen verschwendet wurden. Im Gegensatz zu den ersten beiden Problemen beinhaltet dieses keine geometrischen Aspekte. Es handelt sich um reines Verhalten, doch es stellt dennoch einen dritten Teil des Laufzeitcodes dar, den der Browser nicht bereitstellte.
Popovers versus Modale
Es hilft, die Terminologie vor dem Betrachten der Syntax festzulegen. Ein Popover ist alles, was oberhalb des Rests der Seite angezeigt wird, relativ zu einem Auslöser positioniert ist und bei einem Klick außerhalb oder mit der Escape-Taste wieder verschwindet. Tooltips, Dropdown-Menüs, Autocomplete-Listen und Kontextmenüs sind alle Popovers mit unterschiedlichem Design, aber denselben drei funktionellen Problemen.
Eine Modalliste teilt das Problem der Überlagerung, ist aber kein Popover, weil sie den Zugriff blockiert. Solange eine Modalliste geöffnet ist, ist der darunterliegende Inhalt unbrauchbar: Benutzer können nicht darauf klicken, mit der Tastatur darauf zugreifen oder darin scrollen. In der Regel gibt es einen Hintergrund, auf den der Fokus innerhalb der Modalliste bleibt, bis sie geschlossen wird. Stellen Sie sich eine „Bestätigen löschen“-Frage vor: Nichts anderes auf der Seite ist nutzbar, bis darauf geantwortet wurde. Ein Popover blockiert hingegen nichts – die Seite bleibt vollständig interaktiv, und der Popover schließt einfach, wenn der Benutzer weitermacht.
Die Spezifikationen fassen diesen Unterschied direkt zusammen:
popover="auto"ermöglicht ein einfaches Schließen ohne Blockierung: Es schließt bei Klick außerhalb oder mit der Escape-Taste und lässt den Fokus frei. Tooltips und Dropdowns gehören in diese Kategorie.popover="manual"bleibt geöffnet, bis Ihr Skript es schließt – es gibt kein einfaches Schließen – was sich für eine dauerhafte Benachrichtigungsanzeige eignet.- Ein mit
.showModal()geöffnetes<dialog>ist die blockierende Variante: Es wird an die Oberste Schicht gebracht, erhält einen Hintergrund, behält den Fokus und macht alles dahinter unbrauchbar. - Durch das Öffnen desselben
<dialog>mit.show()entsteht hingegen ein nicht-blockierendes Element, das sich stark wie ein Popover verhält.
Was der JavaScript-Ansatz kostet
Die folgenden, komprimierten Größen stammen von npm und beinhalten nur die Bibliotheken selbst:
popper.js (v1, now deprecated) 7.1 KB
Tippy.js (bundles @popperjs/core) 14.1 KB
Floating UI, vanilla (@floating-ui/dom) 8.1 KB
Floating UI, React bindings 30.1 KB
react-tooltip (@floating-ui/dom + clsx) 14.1 KB
Diese Zahlen beinhalten nicht alles, was zusätzlich hinzugefügt wird: Konfigurationen, das Wrapper-Komponente, CSS für Pfeile und Themes. Das ist die Grundkostenbasis vor jeglicher eigenen Logik – ein Projekt, das beispielsweise ein Tooltip-Paket mit einem separaten Dropdown-Paket kombiniert, zahlt diesen Betrag doppelt.
Keines davon spiegelt mangelhafte Ingenieursarbeit wider. Insbesondere die Floating-UI-Implementierung wurde sorgfältig entwickelt; ihre Hauptaufgabe besteht darin, das Verhalten bei Überlauf in allen Browsern korrekt zu gestalten. Die Kosten entstanden dadurch, dass drei unabhängige Probleme in JavaScript gemeinsam gelöst werden mussten, da die Plattform nichts anderes anbot.
Die Popover-API kümmert sich um Stapeln und Schließen
Zwei separate Spezifikationen haben die Bibliothek ersetzt, und sie teilen die Aufgaben nicht so auf, wie man es erwarten würde. Das popover-Attribut kümmert sich gleichzeitig um das Stapeln und Schließen, mit fast keinem zusätzlichen Script.
<button popovertarget="my-tooltip">Hover me</button>
<div id="my-tooltip" popover="auto">
This is the tooltip content.
</div>
Das Attribut popovertarget verbindet die Schaltfläche mit dem Element, das die entsprechende id-Angabe hat. Mit popover="auto" wird das Element beim Öffnen in die oberste Schicht des Browsers verschoben – das ist genau der Ausweg aus overflow: hidden sowie den zuvor beschriebenen Stapelkontexten – und es erhält außerdem eine einfache Schließfunktion: Klicks außerhalb sowie die Taste Escape schließen es ohne jeglichen Zuhörerkodex.
Eine Korrektur zur Formulierung in der Dokumentation: popovertarget wird bei Aktivierung aktiviert – also durch Klick, Tippen oder Tastenbetätigung – und nicht beim Überfahren mit dem Cursor. Ein echtes Hover-Hinweistextfeld benötigt weiterhin etwas Script-Code, um bei Cursor- und Fokusereignissen showPopover() sowie hidePopover() aufzurufen, oder ein neueres deklaratives Verfahren, sobald die Zielbrowser es unterstützen. Dennoch sind Stapeln und Schließen solcher Elemente nicht mehr auf eine Bibliothek angewiesen. Die Positionierung bleibt das noch ungelöste Problem und gehört zu einer anderen Spezifikation.
Ankerpositionierung verknüpft Elemente nach Namen
CSS-Ankerpositionierung behebt das einzige Problem, das die Popover-API unberücksichtigt lässt. Sie ermöglicht es zwei beliebigen Elementen im Dokument, sich gegenseitig über Namen zu beziehen, anstatt über Eltern- und Kindstrukturen.
.trigger {
anchor-name: --my-anchor;
}
.tooltip {
position: absolute;
position-anchor: --my-anchor;
top: anchor(--my-anchor bottom);
left: anchor(--my-anchor left);
}
anchor-name registriert den Auslöser unter einem durchgestrichenen Identifikator, derselben Syntax wie bei benutzerdefinierten Eigenschaften. position-anchor im Tooltip weist auf diesen Namen hin, und die anchor()-Funktion liest eine bestimmte Kante des Ankers (top, right, bottom, left oder center), damit sich das Tooltip daran ausrichten kann.
Wichtig ist, dass keines der Elemente das andere enthalten muss. Der Browser erledigt nun nativ das, was früher getBoundingClientRect() bei jedem Scrollvorgang manuell berechnen musste. Wenn Sie dies mit einem Popover kombinieren, beachten Sie, dass die Benutzeragenten-Sheetstyle den [popover]-Elementen inset: 0 und margin: auto zuweist, um sie zentriert darzustellen; wenn das Tooltip die von Ihnen festgelegten Ankerabstände ignoriert, ist es in der Regel ausreichend, diese Eigenschaften zurückzusetzen.
Auslösen des Überlaufverhaltens ohne Scroll-Listener
Der Teil einer Positionierungsbibliothek, der den größten Teil der Logik enthält, ist die Handhabung von Überläufen: Es wird erkannt, dass das Tooltip kurz davor steht, den Ansichtsbereich zu verlassen, und zunächst eine andere Platzierung gewählt. Die Ankerpositionierung bewältigt dies mit position-try-fallbacks.
.tooltip {
position: absolute;
position-anchor: --my-anchor;
position-area: top center;
position-try-fallbacks: flip-block, flip-inline;
}
Hier setzt position-area: top center die Standardplatzierung, und position-try-fallbacks listet Alternativen auf, die der Browser nacheinander ausprobiert, falls diese Platzierung ihren Container oder den Ansichtsbereich überschreiten würde. flip-block spiegelt das Tooltip entlang der Blockachse wider, sodass „oben“ zu „unten“ wird, und flip-inline spiegelt es entlang der Inlinenachse wider, sodass „links“ zu „rechts“ wird. Der Browser bewertet dies während der Layout-Phase erneut, ohne dass ein Scroll-Handler oder ein Skript auf der Hauptthread-Ebene den Überlauf erkennt.
Wenn ein einfaches Spiegelbild nicht ausreicht, ermöglicht die at-Regel @position-try es Ihnen, benannte Ersatzplatzierungen zu definieren – jeweils kleine Blöcke mit Positionierungserklärungen, durch die der Browser schalten kann.
@position-try --below {
position-area: bottom center;
margin-top: 8px;
}
@position-try --above {
position-area: top center;
margin-bottom: 8px;
}
.tooltip {
position-anchor: --my-anchor;
position-try-fallbacks: --above, --below;
}
Das ist dieselbe Entscheidung, die Floating UI bei jedem Scroll-Ereignis in JavaScript trifft; sie wird im Voraus als Daten deklariert, die vom Layout-Engine ausgewertet werden. Beachten Sie, dass die .tooltip-Regel in diesem Auszug voraussetzt, dass das Element bereits absolut oder fest positioniert ist, wie in den früheren Beispielen; die Ankerpositionierung hat keinen Einfluss auf statisch positionierte Elemente.
Wann eine Positionierungsbibliothek immer noch sinnvoll ist
Für Tooltips, einfache Dropdowns oder Autocomplete-Listen ist die native Kombination heute eine sinnvolle Standardlösung. Die Rolle der Bibliothek hat zwar abgenommen, ist aber nicht verschwunden.
Die Browserunterstützung ist die erste Einschränkung. Chromium-Browser unterstützen die Positionierung von Ankerpunkten bereits seit Version 125, und @position-try erreichte die Baseline später als anchor() selbst. Die Unterstützung in Safari und Firefox folgte danach, wobei die auf der Webseite angegebenen Zahlen variieren; daher sollte man lieber eine aktuelle Kompatibilitätsliste prüfen, anstatt sich auf eine einzige Versionenliste zu verlassen. Wo die Unterstützung fehlt, gibt es keine sanfte Degradierung ausschließlich über CSS: Ein Browser, der anchor() nicht versteht, kann den Elementpositionierung einfach nicht durchführen. Falls Sie ältere Safari-Versionen oder mobile Browser mit älteren Engines unterstützen müssen, sollten Sie einen Ersatzweg bereithalten; unsere Anleitung zur sicheren Implementierung moderner CSS behandelt die Funktionserkennung sowie den schrittweisen Ausbau für Ankerpunkte.
Der zweite Fall sind Platzierungsregeln, die über das einfache Spiegeln bei Überlauf hinausgehen: ein schwebendes Panel, das eine virtualisierte Liste enthält, Kollisionen, die gleichzeitig gegen mehrere Grenzen überprüft werden, oder eine Platzierung, die von Anwendungsdaten statt vom Layout gesteuert wird. Skripte können auf den jeweiligen Zustand der Anwendung reagieren, während eine feste CSS-Fallback-Liste nur vom Layout Kenntnis hat.
Im gewöhnlichen Fall, der die meisten Projekte umfasst, ist es schwer zu rechtfertigen, 8 bis 30 KB an JavaScript für Aufgaben zu verwenden, die der Browser heute bereits selbst ausführt.
Haupterkenntnisse
- Eine Hilfetextanzeige birgt drei Probleme: Stapeln, Positionieren und Zurückziehen. Bibliotheken existierten, weil all diese Probleme im Skript gelöst werden mussten.
popover="manual" und zusammen mit .showModal() decken die Fälle persistenter und blockierender Elemente ab.position-try-fallbacks zusammen mit @position-try ersetzen die Logik zur Anzeige bei Überlauf, die früher in Bibliotheken verwendet wurde.