Tooltips und Popovers korrekt positionieren
Tooltips, die am Rand des Viewports abgeschnitten werden, Dropdowns, die hinter anderen Elementen verschwinden, und Popovers, die nicht mit dem Scroll-Position synchron bleiben – all das löst das x-anchor-Plugin von Alpine.js mit der Floating-UI-Engine, automatischem Flip-Verhalten und Scroll-Awareness.
Inhaltsverzeichnis
- 1. Das Problem: Manuelle Positionierung bricht immer irgendwann
- 2. x-anchor: Installation und Grundprinzip
- 3. Placement-Optionen: 12 Positionen und auto-Flip
- 4. Tooltip-Pattern: Hover-basiert mit Verzögerung
- 5. Popover-Pattern: Klick-basiert mit Außen-Schließen
- 6. Dropdown-Menü mit x-anchor
- 7. Offset, Pfeil und Styling
- 8. Barrierefreiheit: ARIA-Attribute für Tooltips und Popovers
- 9. x-anchor vs. manuelle Positionierung vs. Tippy.js
- 10. Zusammenfassung
- 11. FAQ
1. Das Problem: Manuelle Positionierung bricht immer irgendwann
Tooltips und Popovers korrekt zu positionieren ist überraschend schwierig. Der naive Ansatz – ein absolut positioniertes Element, das mit CSS relativ zum Anker-Element platziert wird – funktioniert in den meisten Fällen, scheitert aber in vorhersehbaren Szenarien: Der Tooltip wird am Viewport-Rand abgeschnitten, weil kein Platz für die bevorzugte Position ist. Das Element scrollt aus dem Sichtfeld, aber das Popover bleibt zurück. Das Anker-Element liegt innerhalb eines scrollbaren Containers, aber das Popover liegt im Body und verliert den Bezug. CSS transform auf einem Elternelement zerstört die absolute Positionierung.
Diese Probleme sind bekannt und gelöst – von Floating UI, einer kleinen JavaScript-Bibliothek, die sich ausschließlich mit der Positionierung von "schwebenden" Elementen befasst. Alpine.js integriert Floating UI über das x-anchor-Plugin: Eine Direktive, die ein Element relativ zu einem Anker-Element positioniert, mit automatischem Flip-Verhalten wenn kein Platz vorhanden ist, Scroll-Awareness und Viewport-Bound-Checks. Das Ergebnis sind Tooltips und Popovers, die sich immer korrekt verhalten – egal wo auf der Seite, egal in welchem Scroll-Zustand.
2. x-anchor: Installation und Grundprinzip
Das x-anchor-Plugin ist ein First-Party-Plugin von Alpine.js und wird wie alle anderen Plugins über Alpine.plugin(anchor) registriert. In NPM-Projekten wird es als @alpinejs/anchor installiert. Das Plugin bringt Floating UI als Abhängigkeit mit – es ist nicht notwendig, Floating UI separat zu installieren. Nach der Registrierung steht die Direktive x-anchor in allen Templates zur Verfügung.
Das Grundprinzip von x-anchor ist einfach: Das schwebende Element (floating) erhält die Direktive, die auf das Anker-Element verweist. Alpine.js berechnet dann die Position des schwebenden Elements relativ zum Anker, berücksichtigt den Viewport und scrollbaren Container und setzt position: absolute sowie die entsprechenden top- und left-Werte. Das schwebende Element muss im DOM korrekt positioniert sein – entweder direkt im Body oder in einem Container mit position: relative und overflow: visible.
// 1. Plugin registrieren
import Alpine from 'alpinejs';
import anchor from '@alpinejs/anchor';
Alpine.plugin(anchor);
Alpine.start();
// 2. Grundlegende Verwendung im HTML
// Das schwebende Element referenziert das Anker-Element über $refs
/*
<div x-data="{ open: false }">
<!-- Anker-Element -->
<button x-ref="trigger" @click="open = !open">
Info anzeigen
</button>
<!-- Schwebendes Element — x-anchor referenziert den Anker über $refs -->
<div
x-show="open"
x-anchor.bottom-start="$refs.trigger"
style="position: absolute; width: 200px;"
class="bg-white border border-slate-200 rounded-lg shadow-lg p-3 text-sm z-50"
>
<p>Dieser Inhalt erscheint unter dem Button.</p>
<p>Bei zu wenig Platz wechselt er automatisch die Seite.</p>
</div>
</div>
*/
// 3. Placement als Modifier — alle Optionen:
// x-anchor.top → oberhalb, horizontal zentriert
// x-anchor.top-start → oberhalb, linksbündig
// x-anchor.top-end → oberhalb, rechtsbündig
// x-anchor.bottom → unterhalb, horizontal zentriert
// x-anchor.bottom-start → unterhalb, linksbündig
// x-anchor.bottom-end → unterhalb, rechtsbündig
// x-anchor.left → links, vertikal zentriert
// x-anchor.left-start → links, oben ausgerichtet
// x-anchor.left-end → links, unten ausgerichtet
// x-anchor.right → rechts, vertikal zentriert
// x-anchor.right-start → rechts, oben ausgerichtet
// x-anchor.right-end → rechts, unten ausgerichtet
3. Placement-Optionen: 12 Positionen und auto-Flip
Das x-anchor-Plugin unterstützt zwölf Positionierungsoptionen, die als Modifier angegeben werden. Die Benennungskonvention folgt Floating UI: top, bottom, left, right für die Seite, optional ergänzt durch -start und -end für die Ausrichtung auf der Achse. top-start bedeutet: oberhalb des Ankers, linksbündig ausgerichtet. bottom-end bedeutet: unterhalb des Ankers, rechtsbündig ausgerichtet.
Der wichtigste Feature des Plugins ist das automatische Flip-Verhalten: Wenn an der angegebenen Position nicht genug Platz im Viewport vorhanden ist, wechselt das Element automatisch auf die gegenüberliegende Seite. Ein Tooltip mit x-anchor.top wechselt auf bottom, wenn das Anker-Element zu nah am oberen Viewport-Rand liegt. Dieses Verhalten ist standardmäßig aktiv und muss nicht konfiguriert werden. Es ist das Verhalten, das bei manueller Positionierung am schwierigsten zu implementieren ist und in den meisten manuellen Lösungen fehlt.
4. Tooltip-Pattern: Hover-basiert mit Verzögerung
Das klassische Tooltip-Pattern zeigt einen kurzen erklärenden Text, wenn der Benutzer mit der Maus über ein Element fährt oder es fokussiert. Gute Tooltips haben eine kurze Einblend-Verzögerung (damit sie nicht bei versehentlichem Hover aufpoppen) und verschwinden sofort beim Verlassen. Das Schließen beim Verlassen des schwebenden Elements selbst – wenn der Benutzer die Maus auf den Tooltip bewegt – muss ebenfalls berücksichtigt werden.
document.addEventListener('alpine:init', () => {
Alpine.data('tooltip', (content, placement = 'top') => ({
visible: false,
placement,
content,
_timer: null,
show() {
clearTimeout(this._timer);
this._timer = setTimeout(() => { this.visible = true; }, 200);
},
hide() {
clearTimeout(this._timer);
this._timer = setTimeout(() => { this.visible = false; }, 100);
},
stayVisible() {
clearTimeout(this._timer);
},
destroy() {
clearTimeout(this._timer);
}
}));
});
/* HTML-Verwendung:
<div
x-data="tooltip('Produkt zur Wunschliste hinzufügen')"
class="relative inline-block"
>
<!-- Anker -->
<button
x-ref="anchor"
@mouseenter="show()"
@mouseleave="hide()"
@focus="show()"
@blur="hide()"
:aria-describedby="visible ? 'tooltip-1' : undefined"
class="..."
>
♥
</button>
<!-- Tooltip -->
<div
x-show="visible"
x-anchor.top="$refs.anchor"
x-transition:enter="transition ease-out duration-150"
x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100"
x-transition:leave="transition ease-in duration-100"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
@mouseenter="stayVisible()"
@mouseleave="hide()"
id="tooltip-1"
role="tooltip"
style="position: absolute; z-index: 50;"
class="bg-slate-900 text-white text-xs px-3 py-1.5 rounded-lg whitespace-nowrap shadow-lg"
x-text="content"
></div>
</div>
*/
5. Popover-Pattern: Klick-basiert mit Außen-Schließen
Popovers sind komplexer als Tooltips: Sie öffnen sich durch Klick statt Hover, enthalten interaktive Inhalte und müssen sich schließen, wenn der Benutzer außerhalb klickt. Das Außen-Schließen implementiert Alpine.js elegant über die .outside-Direktive: @click.outside="open = false" auf dem Popover-Element löst aus, wenn irgendwo außerhalb des Elements geklickt wird. Die Kombination mit x-anchor positioniert das Popover korrekt, egal wo auf der Seite es geöffnet wird.
document.addEventListener('alpine:init', () => {
Alpine.data('popover', () => ({
open: false,
toggle() { this.open = !this.open; },
close() { this.open = false; },
init() {
// Escape-Taste schließt Popover
this._keyHandler = (e) => {
if (e.key === 'Escape' && this.open) {
this.open = false;
this.$refs.trigger?.focus(); // Fokus zurück zum Trigger
}
};
document.addEventListener('keydown', this._keyHandler);
},
destroy() {
document.removeEventListener('keydown', this._keyHandler);
}
}));
});
/* HTML-Verwendung:
<div x-data="popover()" class="relative inline-block">
<!-- Trigger -->
<button
x-ref="trigger"
@click="toggle()"
:aria-expanded="open"
:aria-controls="open ? 'popover-content' : undefined"
class="flex items-center gap-1 text-sm font-medium text-slate-700 hover:text-teal-600"
>
Mehr Info
<svg class="w-4 h-4 transition-transform" :class="open ? 'rotate-180' : ''"
fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
</svg>
</button>
<!-- Popover -->
<div
x-show="open"
x-anchor.bottom-start="$refs.trigger"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 -translate-y-2"
x-transition:enter-end="opacity-100 translate-y-0"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
@click.outside="close()"
id="popover-content"
role="dialog"
:aria-label="open ? 'Mehr Informationen' : undefined"
style="position: absolute; z-index: 50; width: 300px;"
class="bg-white border border-slate-200 rounded-2xl shadow-xl p-5"
>
<h3 class="font-semibold text-slate-900 mb-2">Produktdetails</h3>
<p class="text-sm text-slate-600 mb-4">Ausführliche Informationen zu Lieferzeit, Materialien und Pflegehinweisen.</p>
<button @click="close()" class="text-xs text-slate-500 hover:text-slate-700">Schließen</button>
</div>
</div>
*/
6. Dropdown-Menü mit x-anchor
Dropdown-Menüs sind spezielle Popovers, die eine Liste von Aktionen oder Links enthalten. Die Positionierung ist kritisch: Das Dropdown muss sich unter dem Trigger-Button öffnen, rechtsbündig ausgerichtet sein (bei Buttons am rechten Rand) und sich nicht mit anderen Seitenelementen überlappen. x-anchor.bottom-end ist für rechtsbündig ausgerichtete Trigger die richtige Option – das Dropdown öffnet sich unterhalb und ist rechtsbündig mit dem Trigger ausgerichtet.
Für Dropdown-Menüs ist Tastaturnavigation besonders wichtig: Pfeiltasten navigieren zwischen den Einträgen, Enter wählt einen aus, Escape schließt das Menü und gibt den Fokus zum Trigger zurück. Die ARIA-Attribute role="menu", role="menuitem" und aria-haspopup="menu" machen das Menü für Screenreader korrekt interpretierbar. Alpine.js erleichtert die Implementierung dieser Tastaturlogik über @keydown-Handler auf dem Container.
7. Offset, Pfeil und Styling
Das x-anchor-Plugin unterstützt einen konfigurierbaren Abstand zwischen Anker und schwebendem Element über den offset-Modifier: x-anchor.bottom.offset.8 fügt 8px Abstand hinzu. Für größere Abstände nutzt man x-anchor.bottom.offset.16. Der Offset-Wert entspricht Tailwind-Spacing-Einheiten: 4 entspricht 16px, 8 entspricht 32px. Dieses Verhalten ist konsistent mit Tailwind, kann aber verwirrend sein, wenn man andere Einheiten erwartet.
Ein Pfeil (Caret) zum Anker-Element ist ein häufiges Design-Element bei Tooltips. Das Plugin bietet keinen eingebauten Pfeil – dieser muss manuell mit CSS implementiert werden. Das gängige Muster ist ein absolut positioniertes Pseudo-Element oder ein rotiertes Div, das am Rand des schwebenden Elements platziert wird. Die exakte Position des Pfeils hängt vom aktuellen Placement ab – für dynamisch wechselnde Placements (durch Flip) kann Alpine.js die aktuelle Position über eine reaktive Variable verfolgen, die im x-anchor-Callback aktualisiert wird.
8. Barrierefreiheit: ARIA-Attribute für Tooltips und Popovers
Tooltips und Popovers haben unterschiedliche ARIA-Anforderungen. Ein Tooltip ist eine nicht-interaktive Beschriftung: role="tooltip" auf dem schwebenden Element, aria-describedby="tooltip-id" auf dem Trigger (nur wenn sichtbar). Der Trigger selbst bleibt der Fokuspunkt – der Tooltip wird nur angezeigt, wenn der Trigger fokussiert oder gehovered ist. Tastaturbenutzer müssen den Tooltip durch Fokussieren des Triggers auslösen können.
Ein Popover enthält interaktive Inhalte und ist damit eine eigenständige Interaktionszone: role="dialog" auf dem schwebenden Element, aria-expanded auf dem Trigger, aria-controls mit der ID des Popovers. Beim Öffnen muss der Fokus in das Popover verschoben werden ($nextTick(() => $el.querySelector('button, a, input')?.focus())), beim Schließen muss er zum Trigger zurückkehren. Das Dropdown-Menü-Muster nutzt role="menu" und role="menuitem", das sich von Dialog unterscheidet und spezifische Tastaturnavigations-Anforderungen hat.
9. x-anchor vs. manuelle Positionierung vs. Tippy.js
Der Vergleich zeigt, wo die Stärken von x-anchor gegenüber Alternativen liegen.
| Aspekt | x-anchor | Manuelles CSS | Tippy.js |
|---|---|---|---|
| Flip-Verhalten | Automatisch | Manuell implementieren | Automatisch |
| Scroll-Awareness | Eingebaut | Scroll-Listener nötig | Eingebaut |
| Alpine-Integration | Nativ | CSS reicht meist | Adapter nötig |
| Bundle-Größe | ~4 KB (mit Floating UI) | 0 KB | ~25 KB |
| Transform-Kompatibilität | Berücksichtigt transforms | Bricht bei transform | Berücksichtigt transforms |
Manuelles CSS-Positioning ist die richtige Wahl, wenn das schwebende Element sich immer in derselben relativen Position befinden soll und kein Flip-Verhalten benötigt wird – etwa bei einem einfachen Dropdown unter einem Button, der immer genug Platz darunter hat. x-anchor ist die richtige Wahl, wenn das Flip-Verhalten wichtig ist oder das Element an unterschiedlichen Stellen auf der Seite verwendet wird. Tippy.js bietet mehr Konfigurationsoptionen, aber auch deutlich mehr Gewicht und eine eigene API, die nicht in das Alpine.js-Programmiermodell integriert ist.
10. Zusammenfassung
Das x-anchor-Plugin von Alpine.js löst das bekannte Problem der Tooltip- und Popover-Positionierung mit der Floating-UI-Engine. Durch automatisches Flip-Verhalten, Scroll-Awareness und Viewport-Bound-Checks produziert das Plugin schwebende Elemente, die sich in allen Viewport-Positionen und Scroll-Zuständen korrekt verhalten. Die Integration in Alpine.js ist nahtlos: Eine Direktive auf dem schwebenden Element, eine $refs-Referenz auf den Anker, und das Plugin übernimmt den Rest.
In der Praxis ist x-anchor die empfohlene Lösung für Tooltips, Popovers und Dropdowns in Hyvä-Projekten und anderen Alpine.js-Applikationen, sobald Flip-Verhalten oder Scroll-Awareness benötigt wird. Für einfache, immer gleich positionierte Dropdowns ist CSS ausreichend. Die Barrierefreiheit muss explizit implementiert werden – das Plugin übernimmt nur die Positionierung, ARIA-Attribute und Tastaturnvigation müssen manuell hinzugefügt werden.
Alpine.js x-anchor — Das Wichtigste auf einen Blick
Plugin registrieren
Alpine.plugin(anchor) vor Alpine.start(). NPM: @alpinejs/anchor. Bringt Floating UI als Abhängigkeit mit.
Direktive verwenden
x-anchor.bottom-start="$refs.trigger" auf dem schwebenden Element. 12 Placement-Optionen als Modifier. Automatischer Flip ohne Konfiguration.
Barrierefreiheit
Tooltip: role="tooltip" + aria-describedby. Popover: role="dialog" + Fokus-Management. Dropdown: role="menu" + Tastaturnavigation.
Offset und Styling
x-anchor.bottom.offset.8 für 8px Abstand. Pfeil/Caret manuell mit CSS implementieren. position: absolute; z-index: 50; am schwebenden Element.
Mironsoft
Alpine.js, Hyvä-Themes und Magento-2-Frontend-Entwicklung
Zugängliche UI-Komponenten für euer Hyvä-Projekt?
Wir bauen Tooltips, Popovers, Dropdowns und Modals mit Alpine.js – zugänglich, CSP-konform und ohne externe Abhängigkeiten. Korrekte Positionierung mit x-anchor inklusive.
Tooltip-Systeme
Hover-Tooltips mit Verzögerung, Flip-Verhalten und ARIA für alle Hyvä-Komponenten
Dropdown-Menüs
Tastaturnavigierbare Dropdown-Menüs mit x-anchor korrekt positioniert
Barrierefreiheit
WCAG 2.1 AA-konforme Interaktionskomponenten mit ARIA und Fokus-Management
11. FAQ: Alpine.js x-anchor
1Was ist x-anchor und warum brauche ich es?
2Separat installieren?
@alpinejs/anchor per NPM. Alpine.plugin(anchor) vor Alpine.start(). Floating UI kommt mit dem Plugin.3Position angeben?
x-anchor.bottom-start, x-anchor.top, x-anchor.right-end etc. 12 Kombinationen möglich.4Was passiert ohne Platz am Viewport-Rand?
top wird zu bottom, right zu left etc. Standardmäßig aktiv.5Abstand zum Anker setzen?
x-anchor.bottom.offset.8 für 8px. Entspricht Tailwind-Spacing-Einheiten (4=16px, 8=32px).6Im Body platzieren?
overflow: hidden-Containern würde das Element abgeschnitten. Für Hyvä direkt im Layout-Template.7Anker referenzieren?
$refs: x-anchor.bottom="$refs.trigger" auf dem schwebenden Element, x-ref="trigger" auf dem Anker. Gleicher x-data-Scope nötig.8ARIA für Tooltips?
role="tooltip" auf dem Element, aria-describedby="id" auf dem Trigger (nur wenn sichtbar). Tooltips sind nicht-interaktiv.9ARIA für Popovers?
role="dialog", aria-expanded + aria-controls auf Trigger. Beim Öffnen Fokus in Dialog, beim Schließen zurück zum Trigger. Escape schließen.