Menús desplegables que se adaptan al viewport

Un panel de acciones revelado o un grupo de botones emergentes es un patrón útil para que los usuarios accedan a funciones adicionales sin ocupar mucho espacio. Este patrón de superposición tiene una complejidad de diseño, ya que el panel debe permanecer vinculado a un elemento activador y, al mismo tiempo, adaptarse a las restricciones del viewport. Tradicionalmente, esto requería bibliotecas de JavaScript complejas (como Popper.js o Floating UI) para calcular las posiciones y controlar las colisiones.

El posicionamiento de anclaje de CSS proporciona una forma declarativa y optimizada para el rendimiento de controlar estas relaciones por completo en CSS, lo que permite que los navegadores administren la lógica de posicionamiento y desbordamiento de forma nativa.

1. Define la relación entre el botón y el panel

El primer paso es crear un botón activador que abra el contenedor de superposición con la API de Popover.

Distinción de accesibilidad OBLIGATORIA: Este patrón modela explícitamente un grupo de botones que se revela dentro de un cuadro emergente en lugar de un menú de ARIA verdadero. No apliques role="menu" ni role="menuitem", a menos que implementes por completo el contrato de navegación con teclado correspondiente (como controlar la navegación espacial con las teclas de flecha entre los elementos). Por el mismo motivo, no agregues aria-haspopup al activador; su valor (menu, listbox, tree, grid o el true heredado) es una promesa de que el destino expone un rol coincidente, lo que este patrón no hace.

<button popovertarget="action-panel">
  Open Actions
</button>
<!-- Use the Popover API (`popover="auto"`) for the overlay to ensure it is placed in the top layer and handled accessibly by the browser. -->
<div id="action-panel" popover="auto" class="panel">
  <button class="action-item" type="button">Edit</button>
</div>

Esto crea una asociación de anclaje implícita entre el botón y el panel, de modo que el panel se pueda posicionar en relación con el botón de forma nativa sin necesidad de anchor-name en el botón ni position-anchor en el panel.

2. Posicionamiento con position-area

En lugar de los desplazamientos manuales de top/left, usa position-area para colocar el objetivo en una cuadrícula de 3 x 3 en relación con el ancla.

.panel {
  /* 
     Position the panel below the anchor (block-end), 
     aligned to the start of the anchor and spanning to its end (span-inline-end).
  */
  position-area: block-end span-inline-end;
  
  /* Reset insets to allow the grid to take control */
  inset: auto;
}

Prefiere las palabras clave lógicas (span-inline-end, block-start) en lugar de las físicas (left, top) para admitir automáticamente la lectura de derecha a izquierda y diferentes modos de escritura.

OBLIGATORIO: No mezcles palabras clave físicas y lógicas en position-area.

3. Implementa la resiliencia perimetral (resguardos)

Para evitar que el panel se corte en el borde de la pantalla, define "tácticas de prueba" que el navegador debe intentar si la posición predeterminada se desborda.

.panel {
  /* 
     If the panel overflows the bottom, flip it to the top (flip-block).
     If it overflows the inline edges, flip it horizontally (flip-inline).
     If it overflows both (e.g., in a corner), flip on both axes (flip-block flip-inline).
  */
  position-try-fallbacks: flip-block, flip-inline, flip-block flip-inline;
}

Si tu menú desplegable o emergente incluye una flecha direccional o un borde que debe cambiar de estilo según la posición de resguardo que se eligió, consulta Información sobre herramientas según la posición. Para la invocación declarativa de diálogos y ventanas emergentes sin JavaScript, consulta Control declarativo de diálogos y ventanas emergentes.

Estrategias de resguardo

Resguardos y compatibilidad de navegadores para Popover

La API de Popover es, en su mayor parte, una mejora progresiva, pero sus comportamientos definitorios (promoción de la capa superior, cierre rápido y la invocación de popovertarget) no tienen un equivalente solo de CSS. Los navegadores más antiguos necesitan un polyfill o un resguardo manual si prefieres no enviar uno.

Polyfill: Para admitir el atributo popover en navegadores más antiguos, carga @oddbird/popover-polyfill de forma condicional. OBLIGATORIO: Detecta la función verificando la propiedad popover en HTMLElement.prototype y carga el polyfill solo cuando no haya compatibilidad nativa. NO lo cargues de forma incondicional.

Con un empaquetador o un mapa de importación:

// MANDATORY: Feature detect 'popover' on HTMLElement.prototype.
if (!("popover" in HTMLElement.prototype)) {
  import("@oddbird/popover-polyfill");
}

Sin un empaquetador, importa desde una CDN dentro de un <script type="module">:

<script type="module">
  if (!("popover" in HTMLElement.prototype)) {
    import("https://unpkg.com/@oddbird/popover-polyfill@latest/dist/popover.min.js");
  }
</script>

Advertencia de diseño: El polyfill no puede definir la pseudoclase :popover-open real, por lo que aplica una clase .\:popover-open en su lugar. OBLIGATORIO: Combina las dos con :is() o :where(); de lo contrario, los navegadores que no tengan :popover-open descartarán toda la regla:

[popover]:is(:popover-open, .\:popover-open) {
  display: block;
}

Como alternativa, para un resguardo heredado sin un polyfill, usa position: fixed y calcula manualmente las coordenadas a través de getBoundingClientRect() o confía en el posicionamiento predeterminado con inset: auto si eso es aceptable para el caso de uso.

Para admitir navegadores sin posicionamiento de anclaje, debes establecer una posición razonable. De forma predeterminada, los popovers se centran en el medio de la pantalla, lo que puede funcionar para tu caso de uso.

En algunos casos de uso, puedes usar el polyfill de @oddbird/css-anchor-positioning, que agrega compatibilidad con algunos casos de uso de posicionamiento de fijación. No admite anclas implícitas, por lo que DEBES agregar nombres de anclas al activador. Además, el polyfill no admite position-area en los popovers, por lo que DEBES usar anchor() en las inserciones deseadas.

<!-- MANDATORY: Conditionally install the anchor positioning polyfill -->
<script type="module">
  if (!("anchorName" in document.documentElement.style)) {
    await import("https://unpkg.com/@oddbird/css-anchor-positioning");
  }
</script>
.panel {
  /* Mandatory: use explicit anchor name */
  position-anchor: --kebab-anchor;
  /* Mandatory: use insets rather than position-area for positioning */
  bottom: auto;
  right: auto;
  top: anchor(bottom);
  left: anchor(left);
  margin: 0;
}