x-data
Alpine
Alpine.js · x-anchor · Tooltips · Popovers · Floating UI
Alpine.js x-anchor
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.

13 Min. Lesezeit x-anchor · Floating UI · Tooltip · Popover · Dropdown · Flip Alpine.js 3.x · Hyvä · Tailwind CSS v4

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>
*/

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?
Plugin für automatische Positionierung schwebender Elemente – mit Flip-Verhalten, Scroll-Awareness und Transform-Kompatibilität. Löst Viewport-Rand-Probleme ohne manuellen Code.
2Separat installieren?
Ja – @alpinejs/anchor per NPM. Alpine.plugin(anchor) vor Alpine.start(). Floating UI kommt mit dem Plugin.
3Position angeben?
Als Modifier: x-anchor.bottom-start, x-anchor.top, x-anchor.right-end etc. 12 Kombinationen möglich.
4Was passiert ohne Platz am Viewport-Rand?
Automatischer Flip zur gegenüberliegenden Seite – 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?
Empfohlen – in overflow: hidden-Containern würde das Element abgeschnitten. Für Hyvä direkt im Layout-Template.
7Anker referenzieren?
Über $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.
10Wann manuelles CSS statt x-anchor?
Wenn kein Flip-Verhalten nötig und Position immer gleich. Einfaches Dropdown unter Button ohne Rand-Probleme braucht kein Plugin.