Zum Inhalt springen

CSS Anchor Positioning: Das Ende von Popper.js

CSS Anchor Positioning positioniert Tooltips, Popovers und Menüs relativ zu jedem Element – ganz ohne JavaScript-Library.

4 Min. Lesezeit
Diagramm eines Tooltips, das mittels CSS Anchor Positioning relativ zu einem Button positioniert wird

Jedes Team, mit dem ich je gearbeitet habe und das Tooltips, Dropdowns oder Popovers ausliefert, liefert am Ende auch eine Positionierungs-Library mit. floating-ui (der Nachfolger von Popper.js) ist hervorragend, aber es löst ein Problem, für das die Plattform nie eine native Antwort hatte: „Platziere dieses Element neben jenem, und klappe es auf die andere Seite, wenn der Platz nicht reicht." Das war's. Das ist der ganze Job. Und über ein Jahrzehnt lang brauchte man dafür JavaScript, das bei jedem Scroll und Resize Bounding Rects neu berechnet.

CSS Anchor Positioning ändert das. Es ist ein nativer Mechanismus, um die Position eines Elements an ein beliebiges anderes Element im DOM zu binden, inklusive eingebauter Fallback-Logik für den Fall, dass das verankerte Element über den Viewport hinausragen würde. Kein ResizeObserver, kein getBoundingClientRect(), keine Re-Render-Schleife.

Das Problem mit Popover-Positionierung heute

Eine typische Tooltip-Implementierung sieht so aus:

jsjavascript
// ❌ JS muss bei jedem Scroll/Resize die Position neu berechnen
import { computePosition, flip, shift, offset } from "@floating-ui/dom";
 
function updateTooltipPosition(button, tooltip) {
  computePosition(button, tooltip, {
    placement: "top",
    middleware: [offset(8), flip(), shift({ padding: 8 })],
  }).then(({ x, y }) => {
    Object.assign(tooltip.style, {
      left: `${x}px`,
      top: `${y}px`,
    });
  });
}
 
button.addEventListener("mouseenter", () =>
  updateTooltipPosition(button, tooltip)
);
window.addEventListener("scroll", () =>
  updateTooltipPosition(button, tooltip)
);
window.addEventListener("resize", () =>
  updateTooltipPosition(button, tooltip)
);

Es funktioniert, kostet aber eine Abhängigkeit, einen Posten in der Bundle-Size-Bilanz und einen Listener auf jedem Scroll-Event in der App. Die CSS Anchor Positioning Spec verlagert diese Logik in die Layout-Engine, wo sie hingehört — der Browser weiß ohnehin schon in jedem Frame, wo sich jede Box befindet.

Ein Element verankern

Der Mechanismus besteht aus zwei Hälften: einem Anchor einen Namen geben, und ein Element relativ dazu positionieren.

csscss
/* Schritt 1: dem Referenzelement einen Namen geben */
.trigger-button {
  anchor-name: --tooltip-anchor;
}
 
/* Schritt 2: dem positionierten Element sagen, welchen Anchor es nutzen soll */
.tooltip {
  position: fixed;
  position-anchor: --tooltip-anchor;
  position-area: top;
  margin-bottom: 8px;
}

anchor-name muss nicht auf einem Parent oder Sibling sitzen — der Anchor und das verankerte Element können an beliebiger Stelle im Dokumentbaum stehen. Allein das eliminiert eine ganze Kategorie von „Warum entkommt position: absolute diesem overflow: hidden-Container nicht?"-Bugs, da sich das verankerte Element relativ zur tatsächlichen Position des Anchors im Viewport positioniert, nicht relativ zu seiner DOM-Abstammung.

position-area akzeptiert Keywords wie top, bottom, left, right sowie Kombinationen wie top left oder bottom span-all, die die verankerte Box auf ein 3x3-Raster um den Anchor herum abbilden. Das ersetzt das manuelle Berechnen von Offsets für jede Platzierungsvariante.

Die Geometrie des Anchors direkt auslesen

Für Layouts, die mehr Präzision brauchen als das Keyword-Raster — etwa ein Popover, das horizontal auf seinem Anchor zentriert bleiben soll, während es darunter positioniert wird — liest die anchor()-Funktion Koordinaten direkt aus der Anchor-Box:

csscss
.tooltip {
  position: fixed;
  position-anchor: --tooltip-anchor;
 
  top: calc(anchor(bottom) + 8px);
  left: anchor(center);
  translate: -50% 0;
}

anchor(bottom) löst sich zur unteren Kante des Anchors auf, anchor(center) zu seinem horizontalen Mittelpunkt — das sind Live-Werte, die der Browser bei jedem Layout-Durchlauf neu berechnet, genau wie %-Werte relativ zu einem Containing Block neu berechnet werden.

Automatische Fallback-Positionierung

Der Teil, der flip() und shift() aus floating-ui tatsächlich ersetzt, ist position-try-fallbacks. Man definiert eine bevorzugte Position und eine Liste von Alternativen; der Browser probiert sie der Reihe nach durch, bis er eine Platzierung findet, die nicht über den Viewport hinausragt.

csscss
.tooltip {
  position: fixed;
  position-anchor: --tooltip-anchor;
  position-area: top;
 
  /* Wenn "top" überlaufen würde, diese der Reihe nach probieren */
  position-try-fallbacks: bottom, left, right;
}

Für Fallbacks, die über die eingebauten Keywords hinaus eigene Logik brauchen, erlaubt @position-try das Definieren eines benannten Blocks von Overrides:

csscss
@position-try --tooltip-bottom-left {
  position-area: bottom left;
  margin-top: 8px;
}
 
.tooltip {
  position-anchor: --tooltip-anchor;
  position-area: top;
  position-try-fallbacks: --tooltip-bottom-left, flip-block;
}
i

flip-block und flip-inline sind eingebaute Fallback-Keywords — sie spiegeln die aktuelle position-area entlang der Block- bzw. Inline-Achse, was die meisten „auf die andere Seite klappen"-Fälle abdeckt, ohne eine eigene @position-try-Regel zu schreiben.

Vorher und nachher: eine echte Tooltip-Komponente

Hier der Unterschied in der Praxis, direkt gegenübergestellt.

AspektJS-Library (floating-ui)CSS Anchor Positioning
Bundle-Kosten~5-8kb gzipped0kb — nativ
Neuberechnung beim ScrollenManueller Listener + rAFAutomatisch, von der Engine gesteuert
Fallback-Platzierungflip()-Middlewareposition-try-fallbacks
Funktioniert in overflow: hidden-VorfahrenBraucht position: fixed-NotlösungenNativ, Anchor-Suche ignoriert DOM-Abstammung
htmlhtml
<button id="info-btn" class="trigger-button">Details</button>
<div id="info-tip" class="tooltip" popover>
  Deployment scheduled for 2:00 AM UTC.
</div>
csscss
.trigger-button {
  anchor-name: --info-anchor;
}
 
.tooltip {
  position-anchor: --info-anchor;
  position-area: top;
  position-try-fallbacks: bottom, flip-inline;
  margin-bottom: 6px;
  padding: 8px 12px;
  border-radius: 6px;
  background: #1e1e1e;
  color: white;
}

Kombiniert man das mit der Popover API — dem popover-Attribut im Markup oben —, bekommt man Open/Close-State-Management, Focus-Handling und Top-Layer-Stacking, ohne eine einzige Zeile JavaScript für das, was früher eine ganze Component-Library gebraucht hat.

Browser-Support und Fallback-Strategie

Anchor Positioning ist ein Baseline-getracktes Feature, das zuerst in Chromium-Browsern landet, mit Implementierungen für Firefox und Safari in Arbeit. Bis es überall verfügbar ist, behandelt man es am besten wie Container Queries im ersten Jahr:

csscss
/* ✅ progressive enhancement, keine harte Abhängigkeit */
.tooltip {
  /* JS-berechnete Fallback-Position als Standard */
  position: fixed;
  top: var(--js-computed-top);
  left: var(--js-computed-left);
}
 
@supports (position-anchor: --info-anchor) {
  .tooltip {
    position-anchor: --info-anchor;
    position-area: top;
    position-try-fallbacks: bottom, flip-inline;
    top: auto;
    left: auto;
  }
}

Prüft den aktuellen Support in der MDN-Referenz zu CSS Anchor Positioning, bevor ihr entscheidet, ob ihr den JS-Fallback-Pfad behaltet oder ihn für ein internes Tool, bei dem ihr den Browser kontrolliert, komplett streicht.

Die wichtigsten Punkte

  1. anchor-name und position-anchor verbinden zwei Elemente an beliebiger Stelle im DOM — anders als bei klassischem position: absolute ist kein gemeinsamer Vorfahre nötig.
  2. position-area deckt das übliche Neun-Positionen-Raster ab; anchor() liefert präzise, berechnete Offsets, wenn Keywords nicht ausreichen.
  3. position-try-fallbacks und @position-try ersetzen die flip()/shift()-Middleware — der Browser probiert bei Überlauf automatisch Alternativen durch.
  4. Kombiniert Anchor Positioning mit der Popover API für Tooltips und Menüs, die weder für Positionierung noch für Open/Close-State JavaScript brauchen.
  5. Liefert es hinter @supports (position-anchor: ...) aus, bis der Support in den Browsern, die laut euren Analytics zählen, einheitlich ist.
  6. Das macht floating-ui für komplexe Fälle nicht überflüssig — virtuelle Anchors, Arrow-Elemente und eigene Middleware brauchen weiterhin JS —, aber es entfernt die Abhängigkeit für die 80% der Popovers, die einfach nur „das hier neben das dort" brauchen.
Wilfredo Rujel

Wilfredo Rujel

Full-Stack-Softwareentwickler

Diesen Beitrag teilenX