CSS Anchor Positioning: El Fin de las Librerías JavaScript para Tooltips (Guía Completa)
Todo desarrollador frontend ha peleado la batalla del posicionamiento de tooltips. Necesitás un menú dropdown pegado a un botón, un tooltip que siga su trigger, o un popover anclado a un elemento específico. La solución siempre fue la misma: instalar una librería JavaScript, calcular coordenadas en cada evento de scroll y resize, manejar la detección de overflow, y rezar para que el z-index no rompa todo.
Floating UI (el sucesor de Popper.js) tiene más de 23 millones de descargas semanales en npm solo para su paquete core. Son millones de proyectos compensando algo que CSS no podía hacer nativamente — hasta ahora.
La API CSS Anchor Positioning alcanzó Baseline 2026 con soporte completo en Chrome 125+, Firefox 147+ y Safari 26. No es una feature experimental detrás de un flag. Está lista para producción, y cambia fundamentalmente cómo construimos elementos UI posicionados en la web.
Esta guía cubre todo: la API principal, estrategias avanzadas de fallback, integración con la Popover API, patrones de migración desde librerías JavaScript, y los edge cases que te van a morder si no estás preparado.
El Problema que Resuelve CSS Anchor Positioning
Antes de esta API, para posicionar un elemento relativo a otro tenías tres opciones:
1. Posicionamiento Absoluto con Offsets Manuales — Frágil, se rompe con scroll, no maneja overflow.
/* El hack clásico */ .tooltip-wrapper { position: relative; } .tooltip { position: absolute; bottom: 100%; left: 50%; transform: translateX(-50%); }
Funciona para layouts estáticos, pero se cae cuando el trigger está cerca del borde del viewport, la página scrollea, o el contenedor tiene overflow: hidden.
2. Cálculo de Posición con JavaScript — El estándar de la industria por una década.
// Lo que Floating UI hace internamente (simplificado) function updatePosition(anchor, floating) { const anchorRect = anchor.getBoundingClientRect(); const floatingRect = floating.getBoundingClientRect(); let top = anchorRect.bottom + 8; let left = anchorRect.left + (anchorRect.width - floatingRect.width) / 2; // Detección de overflow if (top + floatingRect.height > window.innerHeight) { top = anchorRect.top - floatingRect.height - 8; // flip arriba } if (left < 0) left = 8; // empujar a la derecha if (left + floatingRect.width > window.innerWidth) { left = window.innerWidth - floatingRect.width - 8; // empujar a la izquierda } floating.style.top = `${top}px`; floating.style.left = `${left}px`; } // Hay que ejecutar en cada scroll, resize y mutación del DOM window.addEventListener('scroll', () => updatePosition(anchor, floating), { passive: true }); window.addEventListener('resize', () => updatePosition(anchor, floating)); const observer = new ResizeObserver(() => updatePosition(anchor, floating)); observer.observe(anchor);
Funciona, pero estás corriendo cálculos de layout en JavaScript en cada frame de scroll. Estás peleando contra el pipeline de renderizado del navegador en vez de trabajar con él.
3. CSS Anchor Positioning — La solución nativa.
.trigger { anchor-name: --my-trigger; } .tooltip { position: fixed; position-anchor: --my-trigger; bottom: anchor(top); left: anchor(center); translate: -50% 0; /* Manejo automático de overflow */ position-try-fallbacks: flip-block; }
Sin JavaScript. Sin listeners de scroll. Sin resize observers. Sin getBoundingClientRect(). El navegador maneja todo dentro del pipeline de renderizado, donde corresponde.
API Principal: Los Tres Bloques Fundamentales
CSS Anchor Positioning se basa en tres conceptos: declarar anchors, posicionar contra anchors y manejar overflow.
1. Declarar un Anchor
Cualquier elemento puede ser un anchor dándole un nombre con la propiedad anchor-name:
.profile-avatar { anchor-name: --avatar; } .settings-gear { anchor-name: --settings-btn; }
Reglas:
- Los nombres usan la sintaxis CSS dashed-ident (prefijo
--). - Un elemento solo puede tener un anchor name a la vez.
- Los anchor names existen en el mismo scope que el elemento posicionado — siguen las reglas del containing block.
También podés declarar anchors inline en HTML:
<button style="anchor-name: --menu-trigger">Menú</button>
2. Posicionar Contra un Anchor
Para posicionar un elemento relativo a un anchor, necesitás tres cosas:
- El elemento posicionado debe tener
position: absoluteoposition: fixed. - Vinculalo al anchor con
position-anchor. - Usá la función
anchor()en propiedades inset (top,right,bottom,left).
.status-badge { position: fixed; position-anchor: --avatar; /* Posicionar en la esquina inferior derecha del avatar */ top: anchor(bottom); left: anchor(right); }
La función anchor() acepta un keyword de lado que se refiere a la geometría del anchor:
Valor de anchor() | Significado |
|---|---|
anchor(top) | El borde superior del anchor |
anchor(bottom) | El borde inferior del anchor |
anchor(left) | El borde izquierdo del anchor |
anchor(right) | El borde derecho del anchor |
anchor(center) | El punto central del anchor (en el eje relevante) |
anchor(start) | Inicio lógico (respeta la dirección de escritura) |
anchor(end) | Fin lógico |
También podés usar anchor() con porcentaje:
.tooltip { position: fixed; position-anchor: --trigger; /* Posicionar al 25% desde el borde izquierdo del anchor */ left: anchor(25%); bottom: anchor(top); }
El Atajo position-area
Para posiciones comunes, position-area ofrece un modelo mental más simple. En vez de pensar en lados individuales, pensá en una grilla 3×3 alrededor del anchor:
┌──────────┬──────────┬──────────┐
│ top left │ top │ top right│
├──────────┼──────────┼──────────┤
│ left │ center │ right │
├──────────┼──────────┼──────────┤
│ bottom │ bottom │ bottom │
│ left │ │ right │
└──────────┴──────────┴──────────┘
/* Tooltip debajo del anchor, centrado */ .tooltip { position: fixed; position-anchor: --trigger; position-area: bottom center; margin-top: 8px; } /* Sidebar a la derecha del anchor */ .sidebar { position: fixed; position-anchor: --panel; position-area: right; } /* Badge de notificación arriba a la derecha */ .badge { position: absolute; position-anchor: --icon; position-area: top right; }
En la mayoría de los casos, position-area es suficiente. Usá las funciones anchor() solo cuando necesitás precisión sub-elemento (como "20% desde el borde izquierdo del anchor").
3. Manejo de Overflow con position-try-fallbacks
Acá es donde CSS Anchor Positioning le pasa el trapo a las librerías JavaScript. position-try-fallbacks le dice al navegador qué hacer cuando el elemento posicionado se saldría de su containing block:
.tooltip { position: fixed; position-anchor: --trigger; position-area: bottom center; margin-top: 8px; /* Si se desborda abajo, voltear arriba */ position-try-fallbacks: flip-block; }
Estrategias built-in:
| Estrategia | Comportamiento |
|---|---|
flip-block | Voltea al lado opuesto en el eje block (arriba ↔ abajo) |
flip-inline | Voltea en el eje inline (izquierda ↔ derecha) |
flip-block flip-inline | Intenta voltear en ambos ejes |
Para más control, definí posiciones fallback personalizadas con @position-try:
@position-try --above { position-area: top center; margin-bottom: 8px; } @position-try --left-side { position-area: left; margin-right: 8px; } @position-try --right-side { position-area: right; margin-left: 8px; } .tooltip { position: fixed; position-anchor: --trigger; position-area: bottom center; margin-top: 8px; /* Probar cada fallback en orden hasta que uno quepa */ position-try-fallbacks: --above, --left-side, --right-side; }
El navegador evalúa cada fallback en orden y elige el primero que no causa overflow. Pasa automáticamente durante el layout — sin JavaScript, sin requestAnimationFrame, sin resize observers.
Integración con la Popover API
CSS Anchor Positioning se pone todavía más interesante combinado con la Popover API de HTML (atributo popover). Juntos, te dan la solución completa de tooltip/dropdown:
<button popovertarget="user-menu" style="anchor-name: --menu-btn"> Configuración ⚙️ </button> <div id="user-menu" popover anchor="menu-btn"> <nav> <a href="/profile">Perfil</a> <a href="/settings">Configuración</a> <a href="/logout">Cerrar sesión</a> </nav> </div>
[popover] { position: fixed; position-anchor: --menu-btn; position-area: bottom left; margin-top: 4px; position-try-fallbacks: flip-block; }
Lo que obtenés gratis:
- Renderizado en top layer — Sin guerras de z-index. Los popovers se renderizan en el top layer del navegador.
- Light dismiss — Click afuera cierra automáticamente el popover.
- Accesible por defecto — Navegación por teclado y gestión de foco manejada por el navegador.
- Anchor positioning — El menú se queda pegado al trigger, maneja overflow, y se ajusta en scroll.
Cero JavaScript. Todo.
Patrón Tooltip con Popover
<button popovertarget="tip" popovertargetaction="toggle" style="anchor-name: --help-btn"> ¿Ayuda? </button> <div id="tip" popover="hint"> Esta acción no se puede deshacer. </div>
#tip { position: fixed; position-anchor: --help-btn; position-area: top center; margin-bottom: 8px; position-try-fallbacks: flip-block, flip-inline; background: var(--surface-inverse); color: var(--text-inverse); padding: 6px 12px; border-radius: 6px; font-size: 0.85rem; max-width: 240px; }
La variante popover="hint" es no-modal y no roba el foco — perfecto para tooltips.
Nota:
popover="hint"forma parte del foco de Interop 2026 y el soporte de navegadores aún está creciendo. Si la compatibilidad cross-browser es crítica, usápopover="manual"con lógica JavaScript de show/hide como fallback.
Patrones del Mundo Real
Patrón 1: Dropdown con Submenús
.nav-item { anchor-name: --nav-item; } .dropdown { position: fixed; position-anchor: --nav-item; position-area: bottom span-right; margin-top: 4px; position-try-fallbacks: flip-block; } .dropdown-item { anchor-name: --sub-trigger; } .submenu { position: fixed; position-anchor: --sub-trigger; position-area: right; margin-left: 2px; position-try-fallbacks: flip-inline; }
Notá el keyword span-right en position-area — extiende el elemento posicionado desde la posición del anchor hacia la derecha, exactamente cómo debe comportarse un dropdown. Los submenús usan flip-inline para voltear de derecha a izquierda cuando se desbordarían del viewport.
Patrón 2: Tooltip de Validación de Formulario
<div class="field"> <label for="email">Email</label> <input id="email" type="email" style="anchor-name: --email-input" required placeholder="[email protected]"> <div class="validation-msg" popover="hint" id="email-error"> Ingresá una dirección de email válida </div> </div>
#email-error { position: fixed; position-anchor: --email-input; position-area: right; margin-left: 12px; position-try-fallbacks: --below-field; background: var(--color-danger-surface); color: var(--color-danger-text); border: 1px solid var(--color-danger-border); padding: 8px 12px; border-radius: 6px; font-size: 0.85rem; max-width: 200px; } @position-try --below-field { position-area: bottom span-right; margin-top: 4px; margin-left: 0; }
En desktop, el mensaje de validación aparece a la derecha del input. En mobile o viewports angostos donde no hay espacio, automáticamente cae debajo del campo. Sin media queries.
Patrón 3: Anotaciones Contextuales (Estilo Google Docs)
.comment-marker { anchor-name: --comment; background: var(--highlight-yellow); } .comment-thread { position: fixed; position-anchor: --comment; position-area: right; margin-left: 24px; width: 280px; position-try-fallbacks: --left-side; } @position-try --left-side { position-area: left; margin-right: 24px; }
Migrando desde Librerías JavaScript
Si usás Floating UI, Tippy.js o similar, acá está cómo se mapean los conceptos:
Floating UI → CSS Anchor Positioning
| Concepto Floating UI | Equivalente CSS |
|---|---|
computePosition() | position-anchor + anchor() |
placement: 'bottom' | position-area: bottom center |
flip() middleware | position-try-fallbacks: flip-block |
shift() middleware | position-try-fallbacks con @position-try custom |
offset(8) | margin-top: 8px (o el margin correspondiente) |
autoUpdate() | Built-in (automático) |
arrow middleware | Pseudo-elemento ::before con anchor() |
Ejemplo de Migración
Antes (Floating UI):
import { computePosition, flip, shift, offset } from '@floating-ui/dom'; const button = document.querySelector('#trigger'); const tooltip = document.querySelector('#tooltip'); function update() { computePosition(button, tooltip, { placement: 'bottom', middleware: [offset(8), flip(), shift({ padding: 5 })], }).then(({ x, y }) => { Object.assign(tooltip.style, { left: `${x}px`, top: `${y}px`, }); }); } const cleanup = autoUpdate(button, tooltip, update);
Después (CSS Anchor Positioning):
<button style="anchor-name: --trigger">Hover acá</button> <div class="tooltip">Contenido del tooltip</div>
.tooltip { position: fixed; position-anchor: --trigger; position-area: bottom center; margin-top: 8px; position-try-fallbacks: flip-block, flip-inline; }
La versión JavaScript te obliga a importar una librería (~3KB min+gzip para el core de Floating UI), configurar listeners de auto-update y limpiarlos al unmount. La versión CSS son tres propiedades y cero JavaScript.
¿Y la Flecha?
Las librerías JavaScript de tooltips normalmente proveen un middleware de arrow. Con CSS Anchor Positioning, usás un pseudo-elemento:
.tooltip { position: fixed; position-anchor: --trigger; position-area: bottom center; margin-top: 12px; position-try-fallbacks: flip-block; } .tooltip::before { content: ''; position: absolute; bottom: 100%; left: anchor(--trigger center); translate: -50% 0; border: 6px solid transparent; border-bottom-color: var(--tooltip-bg); }
Lo clave: el pseudo-elemento de la flecha usa anchor() para mantenerse alineado con el centro del trigger, incluso si el tooltip se mueve por el manejo de overflow. Una propiedad CSS donde antes hacía falta middleware dedicado.
Edge Cases y Gotchas
1. Scope y Visibilidad del Anchor
Los anchors deben estar en el mismo scope de containing block que el elemento posicionado. Si tu anchor está dentro de un contenedor con overflow: hidden, el elemento posicionado no va a poder "ver" afuera.
Solución: Usá position: fixed para el elemento posicionado.
2. Múltiples Elementos con el Mismo Anchor Name
Si múltiples elementos tienen el mismo anchor-name, solo el último en orden DOM se convierte en el anchor activo.
Solución: Usá nombres de anchor únicos.
3. Anchoring Implícito con el Atributo anchor
<button id="my-btn">Click</button> <div anchor="my-btn" class="popup">Contenido</div>
.popup { position: fixed; position-area: bottom center; }
Ojo: el atributo HTML anchor usa el ID del elemento (sin --), mientras que anchor-name en CSS usa el dashed-ident (con --). No los mezcles.
4. Consideraciones de Animación
Para animar tooltips anclados, usá @starting-style para definir el estado inicial de la transición:
.tooltip { position: fixed; position-anchor: --trigger; position-area: bottom center; opacity: 0; transform: translateY(-4px); transition: opacity 0.2s, transform 0.2s; } .tooltip:popover-open { opacity: 1; transform: translateY(0); } @starting-style { .tooltip:popover-open { opacity: 0; transform: translateY(-4px); } }
5. Cambio Dinámico de Anchor
Podés cambiar a qué anchor se posiciona un elemento dinámicamente:
.contextual-toolbar { position: fixed; position-anchor: --active-selection; position-area: top center; margin-bottom: 8px; }
document.addEventListener('selectionchange', () => { const selection = window.getSelection(); if (selection.rangeCount > 0) { const range = selection.getRangeAt(0); const marker = document.getElementById('selection-marker'); const rect = range.getBoundingClientRect(); marker.style.cssText = ` anchor-name: --active-selection; position: fixed; top: ${rect.top}px; left: ${rect.left}px; width: ${rect.width}px; height: ${rect.height}px; pointer-events: none; `; } });
Útil para toolbars contextuales estilo Medium donde la posición del anchor cambia según la interacción.
Performance: Por Qué lo Nativo le Gana a JavaScript
La diferencia no es marginal — es de arquitectura.
Pipeline de Posicionamiento JavaScript
Scroll del usuario
→ Se dispara evento scroll
→ JavaScript ejecuta getBoundingClientRect() (fuerza layout)
→ JavaScript calcula nueva posición
→ JavaScript actualiza element.style (trigger de layout)
→ Navegador re-renderiza
Cada frame de scroll: Layout → JS → Layout → Paint → Composite
Pipeline de CSS Anchor Positioning
Scroll del usuario
→ Navegador actualiza posiciones en el pase de layout normal
→ Paint → Composite
Cada frame de scroll: Layout (incluye posicionamiento) → Paint → Composite
En la Práctica
Para una página con 20 tooltips/popovers:
- JavaScript: 20 listeners de scroll, cada uno forzando recalcular layout. Resultado: caídas de frames visibles en scroll rápido.
- CSS: Cero listeners. 20 posiciones resueltas en un solo pase de layout. Cero ejecución JavaScript durante scroll.
En dispositivos móviles donde cada milisegundo de main thread importa, esta diferencia es la que separa scroll suave a 60fps de jank visible.
Soporte de Navegadores y Mejora Progresiva
A marzo 2026, CSS Anchor Positioning tiene soporte completo en:
- Chrome/Edge 125+ (desde junio 2024)
- Firefox 147+ (desde principios de 2026)
- Safari 26+ (desde principios de 2026)
Para navegadores antiguos, usá @supports:
/* Fallback */ .tooltip { position: absolute; bottom: 100%; left: 50%; transform: translateX(-50%); } /* Mejora progresiva */ @supports (anchor-name: --a) { .tooltip { position: fixed; position-anchor: --trigger; position-area: top center; margin-bottom: 8px; translate: none; inset: auto; position-try-fallbacks: flip-block; } }
Conclusión
CSS Anchor Positioning es la primitiva de layout más significativa agregada a CSS desde Flexbox y Grid. Resuelve un problema que requirió JavaScript por más de una década — posicionar elementos relativos a otros elementos con manejo de overflow.
La API es engañosamente simple. Tres propiedades (anchor-name, position-anchor, position-area) cubren el 80% de los casos de uso. Los fallbacks @position-try manejan el 20% restante. Combinado con la Popover API, tenés tooltips, dropdowns y popovers completos sin una línea de JavaScript.
Para proyectos nuevos en 2026, no hay razón para instalar Floating UI, Popper.js o Tippy.js para posicionamiento anclado básico. El navegador lo hace nativamente, con mejor rendimiento y menos código.
Para proyectos existentes, la migración es directa: reemplazá las llamadas a computePosition() con propiedades CSS, eliminá los listeners de scroll/resize y borrá la librería de posicionamiento de tus dependencias. Tus usuarios obtienen scroll más suave, tu bundle se achica y tu código se simplifica.
La guerra del posicionamiento de tooltips se acabó. CSS ganó.
Explora herramientas relacionadas
Prueba estas herramientas gratuitas de Pockit