Back

Creando Popovers Accesibles con CSS y JS Modernos

Creando Popovers Accesibles con CSS y JS Modernos

Los popovers presentan información contextual sin interrumpir el flujo de trabajo del usuario, pero implementarlos de manera accesible sigue siendo un desafío para muchos desarrolladores. Ya sea que estés modernizando código heredado o construyendo una biblioteca de componentes, entender la diferencia entre popovers, tooltips y modales es crucial para crear la experiencia de usuario correcta.

Este artículo cubre cómo construir popovers accesibles usando CSS y JavaScript modernos, desde el posicionamiento dinámico hasta la navegación por teclado, mientras exploramos APIs nativas del navegador que reducen la complejidad.

Puntos Clave

  • Los popovers muestran contenido rico e interactivo que persiste hasta ser descartado, a diferencia de los tooltips que muestran pistas breves al pasar el cursor
  • La API nativa Popover elimina la complejidad de JavaScript mientras proporciona características de accesibilidad integradas
  • La gestión adecuada del foco y los atributos ARIA son esenciales para la navegación por teclado
  • El posicionamiento dinámico asegura que los popovers permanezcan visibles dentro de los límites del viewport

Entendiendo Popovers vs. Tooltips vs. Modales

Los Tooltips proporcionan pistas breves al pasar el cursor, típicamente conteniendo una sola línea de texto. Desaparecen cuando los usuarios mueven el cursor y no pueden contener elementos interactivos.

Los Popovers muestran contenido más rico: encabezados, párrafos, botones o formularios. Permanecen visibles hasta ser explícitamente descartados, permitiendo a los usuarios interactuar tanto con el contenido del popover como con la página debajo.

Los Modales crean una experiencia enfocada haciendo que el fondo sea inerte. Los usuarios deben completar la interacción modal antes de regresar al contenido principal.

Requisitos de Implementación Fundamentales

Posicionamiento Dinámico Dentro del Viewport

Los popovers modernos deben adaptarse al espacio disponible en pantalla. Cuando un popover se extendería más allá del borde del viewport, debe reposicionarse automáticamente:

const positionPopover = (trigger, popover) => {
  const triggerRect = trigger.getBoundingClientRect()
  const popoverRect = popover.getBoundingClientRect()
  
  let top = triggerRect.bottom + 8
  let left = triggerRect.left
  
  // Flip to top if insufficient space below
  if (top + popoverRect.height > window.innerHeight) {
    top = triggerRect.top - popoverRect.height - 8
    popover.classList.add('popover--top')
  }
  
  // Adjust horizontal position
  if (left + popoverRect.width > window.innerWidth) {
    left = window.innerWidth - popoverRect.width - 16
  }
  
  popover.style.top = `${top}px`
  popover.style.left = `${left}px`
}

Alineación de Flecha

CSS maneja la flecha visual que apunta al elemento disparador:

.popover::after {
  content: "";
  position: absolute;
  width: 12px;
  height: 12px;
  background: inherit;
  border: inherit;
  transform: rotate(45deg);
  top: -7px;
  left: 20px;
  border-bottom: 0;
  border-right: 0;
}

.popover--top::after {
  top: auto;
  bottom: -7px;
  transform: rotate(225deg);
}

Mecanismos de Descarte

Los popovers accesibles requieren múltiples métodos de descarte:

// Click outside
document.addEventListener('click', (e) => {
  if (!popover.contains(e.target) && !trigger.contains(e.target)) {
    closePopover()
  }
})

// ESC key
document.addEventListener('keydown', (e) => {
  if (e.key === 'Escape' && isPopoverOpen) {
    closePopover()
    trigger.focus() // Return focus to trigger
  }
})

La API Nativa Popover

La API Popover elimina gran parte de la complejidad de JavaScript:

<button popovertarget="my-popover">Open Info</button>

<div id="my-popover" popover>
  <h3>Additional Information</h3>
  <p>This popover requires no JavaScript for basic functionality.</p>
  <button popovertarget="my-popover">Close</button>
</div>

Este enfoque nativo maneja el posicionamiento, descarte y gestión del foco automáticamente. Para mejorar la accesibilidad, combínalo con el elemento <dialog>:

<dialog id="enhanced-popover" popover>
  <h2>Accessible Popover</h2>
  <p>Combining dialog with popover provides semantic meaning.</p>
  <button popovertarget="enhanced-popover">Close</button>
</dialog>

Comparando Soluciones de Bibliotecas vs. Nativas

Las bibliotecas tradicionales como Popper.js ofrecen algoritmos de posicionamiento extensivos pero agregan 15-30KB a tu bundle. La API nativa Popover proporciona:

  • Cero JavaScript para funcionalidad básica
  • Características de accesibilidad integradas
  • Gestión automática del foco
  • Posicionamiento optimizado por el navegador

Para requisitos complejos de posicionamiento, las bibliotecas siguen siendo valiosas. Para casos de uso estándar, las soluciones nativas reducen significativamente la complejidad.

Consideraciones Esenciales de Accesibilidad

Atributos ARIA

Al construir popovers personalizados sin la API nativa:

<button 
  aria-expanded="false"
  aria-controls="custom-popover"
  aria-haspopup="dialog">
  Open Popover
</button>

<div 
  id="custom-popover"
  role="dialog"
  aria-labelledby="popover-title"
  aria-modal="false">
  <h2 id="popover-title">Popover Title</h2>
  <!-- Content -->
</div>

Gestión del Foco

El orden adecuado del foco asegura que los usuarios de teclado puedan navegar efectivamente:

const focusableElements = popover.querySelectorAll(
  'a, button, input, textarea, select, [tabindex]:not([tabindex="-1"])'
)

// Trap focus within popover
popover.addEventListener('keydown', (e) => {
  if (e.key === 'Tab') {
    const firstElement = focusableElements[0]
    const lastElement = focusableElements[focusableElements.length - 1]
    
    if (e.shiftKey && document.activeElement === firstElement) {
      e.preventDefault()
      lastElement.focus()
    } else if (!e.shiftKey && document.activeElement === lastElement) {
      e.preventDefault()
      firstElement.focus()
    }
  }
})

Previniendo el Desplazamiento del Fondo

Solo CSS puede prevenir el desplazamiento del fondo cuando se usa la API nativa:

body:has(dialog[popover]:popover-open) {
  overflow: hidden;
}

Conclusión

Construir popovers accesibles requiere equilibrar las necesidades del usuario con la implementación técnica. La API nativa Popover simplifica el desarrollo mientras mantiene estándares de accesibilidad, aunque las soluciones personalizadas siguen siendo necesarias para interacciones complejas.

Enfócate en la navegación por teclado, implementación adecuada de ARIA y patrones claros de descarte. Ya sea usando APIs nativas o construyendo componentes personalizados, la accesibilidad debe guiar tus decisiones de implementación, asegurando que tus popovers funcionen para todos los usuarios, independientemente de cómo interactúen con tu interfaz.

Preguntas Frecuentes

Usa la API nativa Popover para implementaciones estándar ya que proporciona accesibilidad integrada y no requiere JavaScript. Elige bibliotecas como Popper.js solo cuando necesites lógica de posicionamiento compleja o debas soportar navegadores antiguos que carecen de soporte para la API nativa.

Los tooltips muestran texto breve al pasar el cursor y desaparecen automáticamente, requiriendo solo etiquetas ARIA simples. Los popovers contienen elementos interactivos, necesitan gestión del foco, múltiples métodos de descarte y atributos ARIA apropiados incluyendo role dialog y aria-modal para asegurar que los lectores de pantalla los anuncien correctamente.

La API Popover tiene soporte en Chrome 114+, Edge 114+ y Safari 17+. El soporte de Firefox está en desarrollo. Siempre verifica la compatibilidad actual del navegador y proporciona respaldos para navegadores no soportados usando detección de características antes de implementar en producción.

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay