CSS Anchor Positioning: el fin de Popper.js
CSS anchor positioning permite posicionar tooltips, popovers y menús relativos a cualquier elemento sin librerías de posicionamiento en JavaScript.

Todos los equipos con los que he trabajado que lanzan tooltips, dropdowns o popovers terminan incluyendo una librería de posicionamiento junto con ellos. floating-ui (el sucesor de Popper.js) es excelente, pero existe para resolver un problema para el que la plataforma nunca tuvo una respuesta nativa: "coloca este elemento junto a aquel, y voltéalo hacia el otro lado si se queda sin espacio". Eso es todo. Ese es todo el trabajo. Y durante más de una década, hacerlo correctamente requería que JavaScript calculara bounding rects en cada scroll y resize.
CSS anchor positioning cambia eso. Es un mecanismo nativo para vincular la posición de un elemento a la de otro elemento en cualquier parte del DOM, con lógica de fallback incorporada para cuando el elemento anclado se saldría del viewport. Sin ResizeObserver, sin getBoundingClientRect(), sin bucle de re-render.
El problema con el posicionamiento de popovers hoy
Una implementación típica de tooltip se ve así:
// ❌ JS has to recompute position on every scroll/resize
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)
);Funciona, pero cuesta una dependencia, una línea más en el tamaño del bundle, y un listener en cada evento de scroll de la app. La especificación de CSS Anchor Positioning traslada esta lógica al motor de layout, que es donde debería estar: el navegador ya sabe dónde está cada caja en cada frame.
Anclando un elemento
El mecanismo tiene dos mitades: nombrar un anchor, y posicionar algo en relación a él.
/* Step 1: give the reference element a name */
.trigger-button {
anchor-name: --tooltip-anchor;
}
/* Step 2: tell the positioned element which anchor to use */
.tooltip {
position: fixed;
position-anchor: --tooltip-anchor;
position-area: top;
margin-bottom: 8px;
}anchor-name no tiene que vivir en un padre o un hermano — el anchor y el elemento anclado pueden estar en cualquier parte del árbol del documento. Solo eso elimina toda una categoría de bugs del tipo "por qué position: absolute no logra escapar de este contenedor con overflow: hidden", ya que el elemento anclado se posiciona en relación a la ubicación real del anchor en el viewport, no a su ascendencia en el DOM.
position-area acepta keywords como top, bottom, left, right, y combinaciones como top left o bottom span-all, mapeando la caja anclada sobre una grilla de 3x3 centrada en el anchor. Reemplaza el cálculo manual de offsets para cada variante de posicionamiento.
Leyendo la geometría del anchor directamente
Para layouts que necesitan más precisión que la grilla de keywords — por ejemplo, centrar horizontalmente un popover sobre su anchor mientras se mantiene debajo — la función anchor() lee coordenadas directamente de la caja del anchor:
.tooltip {
position: fixed;
position-anchor: --tooltip-anchor;
top: calc(anchor(bottom) + 8px);
left: anchor(center);
translate: -50% 0;
}anchor(bottom) se resuelve al borde inferior del anchor, anchor(center) a su punto medio horizontal — son valores vivos que el navegador recalcula en cada pasada de layout, de la misma forma que los valores en % se recalculan contra un containing block.
Posicionamiento de fallback automático
La parte que realmente reemplaza flip() y shift() de floating-ui es position-try-fallbacks. Defines una posición preferida y una lista de alternativas; el navegador prueba cada una en orden hasta encontrar una posición que no se salga del viewport.
.tooltip {
position: fixed;
position-anchor: --tooltip-anchor;
position-area: top;
/* If "top" would overflow, try these in order */
position-try-fallbacks: bottom, left, right;
}Para fallbacks que necesitan lógica personalizada más allá de las keywords incorporadas, @position-try te permite definir un bloque nombrado de 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 y flip-inline son keywords de fallback incorporadas — reflejan el
position-area actual a través del eje block o inline, lo que cubre la mayoría
de los casos de "voltear hacia el otro lado" sin necesidad de escribir una
regla @position-try personalizada.
Antes y después: un componente de tooltip real
Esta es la diferencia en la práctica, lado a lado.
| Aspecto | Librería JS (floating-ui) | CSS anchor positioning |
|---|---|---|
| Costo de bundle | ~5-8kb gzipped | 0kb — nativo |
| Recálculo en scroll | Listener manual + rAF | Automático, manejado por el motor |
| Posicionamiento de fallback | Middleware flip() | position-try-fallbacks |
Funciona dentro de un ancestro con overflow: hidden | Requiere escape hatches con position: fixed | Nativo, la búsqueda del anchor ignora la ascendencia en el DOM |
<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;
}Combina esto con la Popover API — el atributo popover en el markup de arriba — y obtienes manejo de estado de apertura/cierre, manejo de foco, y apilamiento en el top layer sin una sola línea de JavaScript, para algo que antes era trabajo de toda una librería de componentes.
Soporte de navegadores y la estrategia de fallback
Anchor positioning es una feature rastreada en Baseline que está llegando primero a los navegadores Chromium, con implementaciones de Firefox y Safari en progreso. Hasta que sea universal, trátala como tratarías las container queries en su primer año:
/* ✅ progressive enhancement, not a hard dependency */
.tooltip {
/* JS-computed fallback position as the default */
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;
}
}Revisa el soporte actual en la referencia de CSS Anchor Positioning de MDN antes de decidir si mantienes la ruta de fallback en JS o la eliminas por completo para una herramienta interna donde controlas el navegador.
Puntos clave
anchor-nameyposition-anchorconectan dos elementos en cualquier parte del DOM — no se requiere un ancestro compartido, a diferencia delposition: absolutetradicional.position-areamaneja la típica grilla de nueve posiciones;anchor()maneja offsets precisos y calculados cuando las keywords no alcanzan.position-try-fallbacksy@position-tryreemplazan el middlewareflip()/shift()— el navegador prueba alternativas automáticamente cuando hay overflow.- Combina anchor positioning con la Popover API para tooltips y menús que no necesitan nada de JavaScript, ni para el posicionamiento ni para el estado de apertura/cierre.
- Lánzalo detrás de
@supports (position-anchor: ...)hasta que el soporte sea uniforme en los navegadores que tu analytics indica que importan. - Esto no mata a floating-ui para casos complejos — anchors virtuales, elementos de flecha y middleware personalizado todavía necesitan JS — pero elimina la dependencia para el 80% de los popovers que solo necesitan "poner esto cerca de aquello".


