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.

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:
// ❌ 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.
/* 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:
.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.
.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:
@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;
}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.
| Aspekt | JS-Library (floating-ui) | CSS Anchor Positioning |
|---|---|---|
| Bundle-Kosten | ~5-8kb gzipped | 0kb — nativ |
| Neuberechnung beim Scrollen | Manueller Listener + rAF | Automatisch, von der Engine gesteuert |
| Fallback-Platzierung | flip()-Middleware | position-try-fallbacks |
Funktioniert in overflow: hidden-Vorfahren | Braucht position: fixed-Notlösungen | Nativ, Anchor-Suche ignoriert DOM-Abstammung |
<button id="info-btn" class="trigger-button">Details</button>
<div id="info-tip" class="tooltip" popover>
Deployment scheduled for 2:00 AM UTC.
</div>.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:
/* ✅ 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
anchor-nameundposition-anchorverbinden zwei Elemente an beliebiger Stelle im DOM — anders als bei klassischemposition: absoluteist kein gemeinsamer Vorfahre nötig.position-areadeckt das übliche Neun-Positionen-Raster ab;anchor()liefert präzise, berechnete Offsets, wenn Keywords nicht ausreichen.position-try-fallbacksund@position-tryersetzen dieflip()/shift()-Middleware — der Browser probiert bei Überlauf automatisch Alternativen durch.- 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.
- Liefert es hinter
@supports (position-anchor: ...)aus, bis der Support in den Browsern, die laut euren Analytics zählen, einheitlich ist. - 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.


