Las Dos Piezas: Anchor Positioning + Popover

Son especificaciones separadas que funcionan bien en conjunto:

CSS Anchor Positioning maneja la pregunta de "¿dónde va el elemento flotante?". Introduce la función anchor(), la propiedad anchor-name, y position-try-fallbacks para reposicionamiento automático.

Popover API maneja la pregunta de "¿cómo se comporta el elemento flotante?". Proporciona renderizado en top-layer (siempre arriba, sin guerras de z-index), light-dismiss (click afuera para cerrar), y semántica de accesibilidad correcta.

Es posible usar cualquiera de las dos de forma independiente, pero juntas reemplazan todo el stack de Floating UI + React Portal + click-outside-handler + manejo de z-index.

Anchor Positioning: Lo Básico

A continuación se presenta el ejemplo más simple posible, un tooltip posicionado debajo de un botón:

/* El anchor (elemento trigger) */
.trigger {
 anchor-name: --my-trigger;
}

/* El elemento posicionado (tooltip) */
.tooltip {
 position: fixed;
 position-anchor: --my-trigger;

 /* Posicionar la esquina top-left del tooltip en el bottom-center del anchor */
 top: anchor(bottom);
 left: anchor(center);
 translate: -50% 0; /* Centrar horizontalmente */

 /* Asegurar que no se desborde del viewport */
 position-try-fallbacks: flip-block;
}
<button class="trigger">Hovereame</button>
<div class="tooltip" popover>¡Soy un tooltip!</div>

La función anchor() toma un keyword de lado (top, right, bottom, left, center) y devuelve la coordenada correspondiente del elemento anchor. Sin getBoundingClientRect(), sin resize observers, sin scroll listeners.

La Función anchor() En Profundidad

La firma de la función es más poderosa de lo que sugiere el ejemplo básico:

.positioned {
 position: fixed;
 position-anchor: --my-anchor;

 /* Básico: posicionar relativo a los bordes del anchor */
 top: anchor(bottom); /* alinear al borde inferior del anchor */
 left: anchor(left); /* alinear al borde izquierdo del anchor */
 right: anchor(right); /* alinear al borde derecho del anchor */

 /* Con porcentaje: interpolar entre bordes */
 left: anchor(50%); /* centro del anchor (igual que anchor(center)) */
 top: anchor(25%); /* 25% desde el top del anchor */

 /* Calc también funciona */
 top: calc(anchor(bottom) + 8px); /* gap de 8px debajo del anchor */
 left: calc(anchor(center) - 20px); /* offset desde el centro */
}

Componente Tooltip del Mundo Real

A continuación se presenta un sistema de tooltips completo con flecha, animación y reposicionamiento automático:

/* Trigger del tooltip */
[data-tooltip-anchor] {
 anchor-name: --tooltip-anchor;
}

/* Contenedor del tooltip */
.tooltip {
 position: fixed;
 position-anchor: --tooltip-anchor;
 position-visibility: anchors-visible;

 /* Posición default: abajo y centrado */
 top: calc(anchor(bottom) + 8px);
 justify-self: anchor-center;

 /* Estilos */
 background: var(--tooltip-bg, #1a1a2e);
 color: var(--tooltip-text, #eee);
 padding: 8px 12px;
 border-radius: 6px;
 font-size: 0.875rem;
 max-width: 280px;
 width: max-content;
 filter: drop-shadow(0 4px 12px rgba(0, 0, 0, 0.15));

 /* Animación */
 opacity: 0;
 transform: translateY(-4px);
 transition: opacity 150ms ease, transform 150ms ease;

 /* Posiciones fallback */
 position-try-fallbacks:
 --tooltip-above,
 --tooltip-left,
 --tooltip-right;
}

/* Estado visible */
.trigger:hover + .tooltip,
.trigger:focus-visible + .tooltip {
 opacity: 1;
 transform: translateY(0);
}

/* Flecha */
.tooltip::before {
 content: '';
 position: absolute;
 bottom: 100%;
 left: 50%;
 translate: -50% 0;
 border: 6px solid transparent;
 border-bottom-color: var(--tooltip-bg, #1a1a2e);
}

/* Posición fallback: arriba */
@position-try --tooltip-above {
 bottom: calc(anchor(top) + 8px);
 top: auto;
 justify-self: anchor-center;
}

/* Posición fallback: izquierda */
@position-try --tooltip-left {
 right: calc(anchor(left) + 8px);
 top: anchor(center);
 left: auto;
 translate: 0 -50%;
}

/* Posición fallback: derecha */
@position-try --tooltip-right {
 left: calc(anchor(right) + 8px);
 top: anchor(center);
 translate: 0 -50%;
}

Cuando el tooltip desbordaría el viewport debajo del botón, el browser automáticamente prueba --tooltip-above, después --tooltip-left, después --tooltip-right. Sin JavaScript. Sin resize observer. Sin scroll handler.

La Popover API: Capacidades y Comportamiento

La Popover API no es solo alternar display: none. Provee tres cosas que son genuinamente difíciles de construir correctamente:

1. Renderizado en Top Layer

Los elementos con el atributo popover se renderizan en el top layer del browser, la misma capa usada por <dialog> y los elementos fullscreen. Esto significa cero problemas de stacking context con z-index. Tu popover siempre se renderiza arriba de todo, incluso si su padre en el DOM tiene overflow: hidden o un z-index bajo.

<div style="overflow: hidden; position: relative; z-index: 1;">
 <button popovertarget="my-popup">Abrir</button>
</div>

<!-- Esto se renderiza en el top layer, sin que le afecte el overflow/z-index del padre -->
<div id="my-popup" popover>
 ¡Soy libre de la cárcel del z-index!
</div>

2. Light Dismiss

popover="auto" (el default) te da light-dismiss automático. Hacer click en cualquier lugar fuera del popover lo cierra. Presionar Escape lo cierra. Abrir otro popover auto cierra el anterior. Este es el comportamiento que cada librería de dropdowns reimplementa, y el browser lo hace correctamente para todas las modalidades de input (mouse, touch, teclado).

<!-- Popover auto: light-dismiss habilitado -->
<button popovertarget="dropdown">Menú</button>
<div id="dropdown" popover="auto">
 <a href="/profile">Perfil</a>
 <a href="/settings">Configuración</a>
 <a href="/logout">Cerrar sesión</a>
</div>

<!-- Popover manual: sin light-dismiss, hay que cerrarlo explícitamente -->
<button popovertarget="notification">Mostrar</button>
<div id="notification" popover="manual">
 Esto se queda hasta que lo cierres.
 <button popovertarget="notification" popovertargetaction="hide">Cerrar</button>
</div>

3. Accesibilidad Incluida

La API de popover maneja el focus management y los atributos ARIA automáticamente. Cuando un popover se abre, se hace accesible para tecnologías asistivas. El botón popovertarget obtiene automáticamente el comportamiento de aria-expanded.

Construyendo un Menú Dropdown Completo

A continuación se presenta un menú dropdown de grado producción usando ambas APIs juntas:

<nav class="main-nav">
 <button
 class="nav-trigger"
 popovertarget="products-menu"
 style="anchor-name: --products-trigger"
 >
 Productos ▾
 </button>

 <div
 id="products-menu"
 class="dropdown-menu"
 popover="auto"
 style="position-anchor: --products-trigger"
 >
 <a href="/products/analytics" class="dropdown-item">
 <div>
 <strong>Analytics</strong>
 <small>Rastreá el comportamiento de usuarios</small>
 </div>
 </a>
 <a href="/products/monitoring" class="dropdown-item">
 <div>
 <strong>Monitoring</strong>
 <small>Alertas en tiempo real</small>
 </div>
 </a>
 <hr class="dropdown-divider" />
 <a href="/products/all" class="dropdown-item">
 Ver todos los productos →
 </a>
 </div>
</nav>
.dropdown-menu {
 position: fixed;
 position-anchor: --products-trigger;

 /* Posicionar debajo del trigger, alineado al borde izquierdo */
 top: calc(anchor(bottom) + 4px);
 left: anchor(left);

 /* Tamaño */
 min-width: anchor-size(width); /* Al menos tan ancho como el trigger */
 max-width: 320px;

 /* Estilos */
 background: white;
 border: 1px solid #e2e2e2;
 border-radius: 8px;
 padding: 4px;
 box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);

 /* Animación */
 opacity: 0;
 transform: translateY(-8px) scale(0.96);
 transition: opacity 200ms ease,
 transform 200ms ease,
 overlay 200ms ease allow-discrete,
 display 200ms ease allow-discrete;

 /* Fallback: flipear arriba si no hay lugar abajo */
 position-try-fallbacks: flip-block;
}

/* Estado abierto */
.dropdown-menu:popover-open {
 opacity: 1;
 transform: translateY(0) scale(1);
}

/* Starting style para la animación de entrada */
@starting-style {
 .dropdown-menu:popover-open {
 opacity: 0;
 transform: translateY(-8px) scale(0.96);
 }
}

Cero JavaScript. El botón alterna el popover. El popover se posiciona solo respecto al botón. Hacer click afuera lo cierra. Si no hay lugar abajo, invierte arriba. La animación de entrada usa @starting-style para animar correctamente de display: none a display: block.

La Función anchor-size()

Una feature subestimada es anchor-size(), que te deja leer las dimensiones de tu elemento anchor:

.dropdown {
 /* Hacer que el dropdown sea al menos tan ancho como el trigger */
 min-width: anchor-size(width);

 /* O matchearlo exacto */
 width: anchor-size(width);

 /* También es posible leer el height */
 max-height: calc(100vh - anchor-size(height) - 16px);
}

Esto es perfecto para dropdowns de autocomplete que deberían coincide conr el ancho del campo de input, o para mega-menús que abarcan el ancho de su item nav trigger.

Estado del Soporte en Navegadores

Dónde estamos a principios de 2026:

Feature Chrome Firefox Safari
Popover API 114+ 125+ 17+
anchor-name / position-anchor 125+ Detrás de flag En desarrollo
anchor() function 125+ Detrás de flag En desarrollo
position-try-fallbacks 125+ Detrás de flag En desarrollo
anchor-size() 125+ Detrás de flag En desarrollo
@position-try 125+ Detrás de flag En desarrollo
position-visibility 131+ No No

La Popover API tiene soporte amplio. Anchor positioning es solo Chrome/Edge en producción. Firefox está cerca (detrás de flag desde la 127), y Safari está trabajando activamente.

Estrategia de Progressive Enhancement

/* Base: funciona en todos lados */
.tooltip {
 position: fixed;
 /* Posicionamiento basado en JS como fallback */
}

/* Mejorado: anchor positioning donde esté soportado */
@supports (anchor-name: --test) {
 .tooltip {
 position-anchor: --trigger;
 top: calc(anchor(bottom) + 8px);
 justify-self: anchor-center;
 position-try-fallbacks: flip-block;
 }
}
// Solo cargar Floating UI si anchor positioning no está soportado
if (!CSS.supports('anchor-name', '--test')) {
 import('floating-ui').then(({ computePosition, flip, offset }) => {
 // Lógica de posicionamiento fallback
 });
}

Así, el 70%+ de los usuarios (Chrome/Edge) obtienen la experiencia nativa con cero JavaScript, y el resto recibe el fallback con Floating UI.

Qué Significa Esto para las Librerías de UI

Cada librería de componentes importante implementa alguna forma de posicionamiento de UI flotante. A medida que anchor positioning gane soporte en browsers, estas librerías lo van a adoptar progresivamente. Algunas ya tienen flags experimentales. El estado final es que la lógica de posicionamiento se mueve del bundle de JavaScript al motor del browser, reduciendo tanto el tamaño del bundle como la computación en runtime.

Para proyectos nuevos, si solo se apunta a browsers Chromium (apps Electron, herramientas internas, extensiones de Chrome), es posible usar anchor positioning hoy con cero fallback necesario. Para sitios web públicos, el approach de progressive enhancement mostrado arriba es el camino pragmático.

La plataforma web finalmente se está poniendo al día con lo que los frameworks de UI nativos hicieron durante décadas, dejar que el motor de layout maneje el posicionamiento de elementos. Tardó más de lo necesario, pero la capacidad ya está disponible.