Become a member!

Funcionalidades HTML Nativas que Pueden Reemplazar tu JavaScript: Popover, Details y Datalist

🌐
Este artículo también está disponible en otros idiomas:
🇬🇧 English  •  🇮🇹 Italiano

TL;DR - Puntos Clave

  • Popover API: Crea menús desplegables, tooltips y overlays modales con simples atributos HTML (popover, popovertarget). El navegador maneja automáticamente la detección de clic externo, gestión del foco, navegación por teclado y z-index.

  • Details/Summary: Construye acordeones accesibles sin JavaScript. Usa el atributo name para crear acordeones exclusivos donde solo una sección puede estar abierta a la vez.

  • Datalist: Añade sugerencias de autocompletado a cualquier campo input vinculándolo a un elemento <datalist>. Funciona con inputs de tipo text, email, number, color y date.

  • Soporte de Navegadores: Las tres funcionalidades tienen más del 90% de soporte global y funcionan en Chrome, Firefox, Safari y Edge.

  • Cuándo usarlas: Comienza con HTML nativo para patrones UI comunes. Solo recurre a JavaScript cuando necesites personalización avanzada, animaciones complejas o conjuntos de datos muy grandes.

Todo desarrollador web ha pasado por esto. Necesitas un simple menú desplegable, un acordeón para tu página de FAQ, o un campo de autocompletado para un input de búsqueda. La funcionalidad parece sencilla, así que empiezas a programar. Unas horas después, estás hundido en gestión de estado, event handlers, atributos ARIA y casos límite que no habías anticipado. Lo que empezó como una “funcionalidad rápida” se ha convertido en una carga de mantenimiento que te perseguirá durante meses.

¿La ironía? El navegador puede manejar la mayor parte de esto por ti. El HTML nativo ha evolucionado drásticamente, y muchos patrones UI comunes ahora tienen soluciones integradas que son más accesibles, más performantes y significativamente menos propensas a bugs que las implementaciones JavaScript personalizadas.

En esta guía, exploraremos tres potentes funcionalidades HTML que pueden reemplazar cantidades sustanciales de código JavaScript: la Popover API, los elementos Details/Summary y el elemento Datalist. No son funcionalidades experimentales ocultas tras flags - son soluciones listas para producción soportadas por todos los navegadores modernos.

El Coste Oculto de los Componentes UI Personalizados

Antes de sumergirnos en las soluciones, reconozcamos el problema real. Cuando implementas un menú desplegable personalizado en JavaScript, no estás solo escribiendo código para mostrar y ocultar contenido. Te estás responsabilizando de:

  • Detección de clic externo: Cuando el usuario hace clic en cualquier lugar fuera de tu menú, debería cerrarse. Suena simple, pero los casos límite se multiplican rápidamente. ¿Y los clics en elementos anidados? ¿Y los clics que empiezan dentro pero terminan fuera durante un drag?

  • Navegación por teclado: Los usuarios esperan cerrar menús con la tecla Escape. Los usuarios de lectores de pantalla esperan una correcta gestión del foco. Los usuarios que solo usan teclado necesitan navegar sin ratón.

  • Focus trapping: Cuando un menú está abierto, el foco no debería escapar a los elementos detrás de él. Pero tampoco debería atrapar usuarios indefinidamente.

  • Gestión de z-index: Tu menú necesita aparecer sobre todo lo demás. Pero ¿qué pasa cuando otro componente también usa valores z-index altos? Bienvenido a la guerra de z-index.

  • Comportamiento de scroll: ¿Qué pasa cuando el usuario hace scroll mientras tu menú está abierto? ¿Y en móvil cuando aparece el teclado virtual?

  • Coordinación de estado: Si el usuario abre un segundo menú, ¿debería cerrarse el primero automáticamente?

Cada una de estas preocupaciones requiere código. Cada línea de código puede tener bugs. Cada bug crea un ticket de soporte. Cada ticket quita tiempo de construir funcionalidades que realmente diferencian tu producto.

El navegador, por otro lado, ha estado resolviendo estos problemas durante décadas. Los desarrolladores de navegadores han encontrado cada caso límite, manejado cada requisito de accesibilidad y optimizado cada cuello de botella de rendimiento. Cuando usas funcionalidades HTML nativas, estás aprovechando esa experiencia colectiva.


La Popover API: Dropdowns, Menús y Tooltips Nativos

Referencia Rápida: Popover API

Atributo Propósito Ejemplo
popover Convierte elemento en popover <div popover>...</div>
popover="auto" Auto-cierre al clic externo Comportamiento por defecto
popover="manual" Solo se cierra programáticamente Para tooltips
popovertarget="id" El botón activa el popover <button popovertarget="menu">
popovertargetaction hide, show, o toggle popovertargetaction="hide"

Soporte de Navegadores: Chrome 114+, Firefox 125+, Safari 17+, Edge 114+ (~90% global)

La Popover API es una de las adiciones más significativas a HTML en los últimos años. Proporciona una forma declarativa de crear contenido que aparece “encima” del resto de la página - perfecto para menús desplegables, tooltips, paneles de notificación y patrones UI similares.

El Problema que Resuelve

Las implementaciones tradicionales de dropdown requieren:

// El dropdown "simple" - lo que realmente requiere
function setupDropdown(buttonId, menuId) {
  const button = document.getElementById(buttonId);
  const menu = document.getElementById(menuId);
  let isOpen = false;

  // Toggle al clic del botón
  button.addEventListener('click', (e) => {
    e.stopPropagation();
    isOpen = !isOpen;
    menu.style.display = isOpen ? 'block' : 'none';
    button.setAttribute('aria-expanded', isOpen);
    if (isOpen) {
      menu.querySelector('a, button')?.focus();
    }
  });

  // Cerrar al clic externo
  document.addEventListener('click', (e) => {
    if (isOpen && !menu.contains(e.target) && e.target !== button) {
      isOpen = false;
      menu.style.display = 'none';
      button.setAttribute('aria-expanded', 'false');
    }
  });

  // Cerrar con tecla Escape
  document.addEventListener('keydown', (e) => {
    if (e.key === 'Escape' && isOpen) {
      isOpen = false;
      menu.style.display = 'none';
      button.setAttribute('aria-expanded', 'false');
      button.focus();
    }
  });

  // Manejar focus trap... (más código)
  // Manejar z-index... (más código)
  // Manejar dropdowns múltiples... (más código)
}

Esta es la versión simplificada. Las implementaciones del mundo real son mucho más complejas.

La Solución Nativa

Con la Popover API, la misma funcionalidad requiere:

<button popovertarget="user-menu">Cuenta</button>

<div id="user-menu" popover>
  <a href="/profile">Perfil</a>
  <a href="/settings">Configuración</a>
  <button popovertarget="user-menu" popovertargetaction="hide">
    Cerrar Sesión
  </button>
</div>

Eso es todo. Sin JavaScript. El navegador maneja:

  • Mostrar y ocultar el popover cuando se hace clic en el botón
  • Cerrar el popover al hacer clic fuera (light dismiss)
  • Cerrar el popover al presionar Escape
  • Correcta gestión del foco
  • Promover el popover al top layer (sin necesidad de z-index)
  • Asegurar que solo un popover auto esté abierto a la vez

Prueba la demo en vivo en CodePen

Comprendiendo los Modos Popover

El atributo popover acepta diferentes valores que controlan el comportamiento:

Modo Auto (por defecto):

<div popover="auto" id="my-popup">
  Este se cierra cuando haces clic fuera o presionas Escape.
  Abrir otro popover auto cierra este.
</div>

<!-- Equivalente a: -->
<div popover id="my-popup">...</div>

Modo Manual:

<div popover="manual" id="my-panel">
  Este permanece abierto hasta que se cierre explícitamente.
  Múltiples popovers manuales pueden estar abiertos simultáneamente.
</div>

El modo manual es perfecto para paneles persistentes, sidebars o notificaciones que no deberían desaparecer al hacer clic fuera.

Control Declarativo con popovertargetaction

El atributo popovertargetaction te permite especificar exactamente qué debería hacer un botón:

<!-- Toggle (por defecto) - abre si está cerrado, cierra si está abierto -->
<button popovertarget="menu">Toggle Menú</button>

<!-- Solo show - no hace nada si ya está abierto -->
<button popovertarget="menu" popovertargetaction="show">Abrir Menú</button>

<!-- Solo hide - no hace nada si ya está cerrado -->
<button popovertarget="menu" popovertargetaction="hide">Cerrar Menú</button>

Esto es particularmente útil para tener un botón “X” de cierre dentro de tu popover:

<div id="notification" popover>
  <p>¡Tienes nuevos mensajes!</p>
  <button popovertarget="notification" popovertargetaction="hide">
    Cerrar
  </button>
</div>

Ejemplo Completo de Menú de Usuario

Aquí hay una implementación de menú de usuario lista para producción:

<!DOCTYPE html>
<html lang="es">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Ejemplo Menú Popover</title>
  <style>
    * { box-sizing: border-box; margin: 0; padding: 0; }

    body {
      font-family: system-ui, -apple-system, sans-serif;
      background: #1a1a2e;
      color: #eee;
      min-height: 100vh;
    }

    header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 1rem 2rem;
      background: rgba(255,255,255,0.05);
    }

    .logo { font-weight: 700; font-size: 1.25rem; }

    .avatar-btn {
      width: 44px;
      height: 44px;
      border-radius: 50%;
      border: 2px solid rgba(255,255,255,0.2);
      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
      color: white;
      font-weight: 700;
      font-size: 1rem;
      cursor: pointer;
      transition: border-color 0.2s;
    }

    .avatar-btn:hover {
      border-color: rgba(255,255,255,0.5);
    }

    .user-menu {
      margin: 0;
      padding: 0.5rem;
      min-width: 220px;
      border: 1px solid rgba(255,255,255,0.1);
      border-radius: 12px;
      background: #2a2a4a;
      box-shadow: 0 20px 60px rgba(0,0,0,0.4);
    }

    .user-menu::backdrop {
      background: transparent;
    }

    .menu-header {
      padding: 0.75rem 1rem;
      border-bottom: 1px solid rgba(255,255,255,0.1);
      font-size: 0.875rem;
      opacity: 0.7;
    }

    .menu-item {
      display: block;
      width: 100%;
      padding: 0.75rem 1rem;
      border: none;
      border-radius: 8px;
      background: transparent;
      color: inherit;
      text-decoration: none;
      text-align: left;
      font-size: 1rem;
      cursor: pointer;
      transition: background 0.15s;
    }

    .menu-item:hover {
      background: rgba(255,255,255,0.1);
    }

    .menu-item.danger:hover {
      background: rgba(255,59,48,0.2);
      color: #ff6b6b;
    }
  </style>
</head>
<body>
  <header>
    <div class="logo">MiApp</div>
    <button class="avatar-btn"
            popovertarget="user-menu"
            aria-label="Abrir menú de usuario">
      DT
    </button>
  </header>

  <div id="user-menu" popover class="user-menu">
    <div class="menu-header">Conectado como Daniele</div>
    <a href="/profile" class="menu-item">Perfil</a>
    <a href="/settings" class="menu-item">Configuración</a>
    <a href="/billing" class="menu-item">Facturación</a>
    <hr style="border-color: rgba(255,255,255,0.1); margin: 0.5rem 0;">
    <button class="menu-item danger"
            popovertarget="user-menu"
            popovertargetaction="hide">
      Cerrar Sesión
    </button>
  </div>
</body>
</html>

Guarda esto como archivo HTML y ábrelo en tu navegador. Haz clic en el botón del avatar para ver el menú. Haz clic fuera o presiona Escape para cerrarlo. Nota lo fluido y confiable que es - con cero JavaScript.

Integración JavaScript Cuando Sea Necesario

A veces necesitas JavaScript para comportamientos específicos del producto. La Popover API proporciona métodos y eventos para esto:

const popover = document.getElementById('my-popover');

// Control programático
popover.showPopover();   // Mostrar el popover
popover.hidePopover();   // Ocultar el popover
popover.togglePopover(); // Toggle visibilidad

// Verificar si está abierto
if (popover.matches(':popover-open')) {
  console.log('El popover está visible');
}

// Escuchar cambios de estado
popover.addEventListener('toggle', (event) => {
  console.log(`El popover ahora está: ${event.newState}`); // 'open' o 'closed'

  if (event.newState === 'open') {
    // Analytics, lazy-load contenido, etc.
  }
});

El punto clave es que JavaScript debería añadir comportamiento del producto, no recrear comportamiento de la plataforma. Deja que el navegador maneje mostrar, ocultar, gestión del foco e interacción por teclado. Usa JavaScript solo para lo que hace única a tu aplicación.

Implementación de Tooltip

Los popovers son perfectos para tooltips. Aquí hay un simple patrón de tooltip activado por hover:

<span class="tooltip-container">
  <button id="help-btn"
          popovertarget="help-tooltip"
          popovertargetaction="toggle">
    ?
  </button>
  <div id="help-tooltip" popover="manual" class="tooltip">
    Este campo acepta tu dirección de email corporativo.
    Contacta TI si necesitas asistencia.
  </div>
</span>

<style>
  .tooltip-container {
    position: relative;
    display: inline-block;
  }

  .tooltip {
    margin: 0;
    padding: 0.5rem 0.75rem;
    position: absolute;
    top: 100%;
    left: 50%;
    transform: translateX(-50%);
    background: #333;
    color: white;
    font-size: 0.875rem;
    border-radius: 6px;
    white-space: nowrap;
    max-width: 250px;
    white-space: normal;
  }

  .tooltip::before {
    content: '';
    position: absolute;
    bottom: 100%;
    left: 50%;
    transform: translateX(-50%);
    border: 6px solid transparent;
    border-bottom-color: #333;
  }
</style>

<script>
  const btn = document.getElementById('help-btn');
  const tooltip = document.getElementById('help-tooltip');

  btn.addEventListener('mouseenter', () => tooltip.showPopover());
  btn.addEventListener('mouseleave', () => tooltip.hidePopover());
  btn.addEventListener('focus', () => tooltip.showPopover());
  btn.addEventListener('blur', () => tooltip.hidePopover());
</script>

Prueba la demo en vivo en CodePen

Usando popover="manual" nos aseguramos de que el tooltip solo se muestre cuando lo controlamos explícitamente vía JavaScript, en lugar de auto-cerrarse al clic externo.

Toast de Notificación

Crea notificaciones dismissibles que se apilen correctamente:

<button onclick="showNotification()">Mostrar Notificación</button>

<div id="notification" popover="manual" class="notification success">
  <span class="notification-icon">✓</span>
  <span class="notification-message">¡Tus cambios han sido guardados!</span>
  <button popovertarget="notification"
          popovertargetaction="hide"
          class="notification-close">
    &times;
  </button>
</div>

<style>
  .notification {
    margin: 0;
    padding: 1rem 1.25rem;
    position: fixed;
    bottom: 1rem;
    right: 1rem;
    display: flex;
    align-items: center;
    gap: 0.75rem;
    min-width: 300px;
    border: none;
    border-radius: 8px;
    box-shadow: 0 10px 40px rgba(0,0,0,0.2);
    animation: slideIn 0.3s ease;
  }

  .notification.success {
    background: #d4edda;
    border-left: 4px solid #28a745;
  }

  .notification.error {
    background: #f8d7da;
    border-left: 4px solid #dc3545;
  }

  .notification-close {
    margin-left: auto;
    background: none;
    border: none;
    font-size: 1.25rem;
    cursor: pointer;
    opacity: 0.5;
  }

  .notification-close:hover {
    opacity: 1;
  }

  @keyframes slideIn {
    from {
      transform: translateX(100%);
      opacity: 0;
    }
    to {
      transform: translateX(0);
      opacity: 1;
    }
  }
</style>

<script>
  function showNotification() {
    const notification = document.getElementById('notification');
    notification.showPopover();

    // Auto-cierre después de 5 segundos
    setTimeout(() => {
      if (notification.matches(':popover-open')) {
        notification.hidePopover();
      }
    }, 5000);
  }
</script>

Prueba la demo en vivo en CodePen

Drawer de Navegación Móvil

Los popovers funcionan excelentemente para navegación deslizante en móvil:

<button class="hamburger"
        popovertarget="mobile-nav"
        popovertargetaction="show"
        aria-label="Abrir navegación">
  <span></span>
  <span></span>
  <span></span>
</button>

<nav id="mobile-nav" popover="manual" class="drawer">
  <div class="drawer-header">
    <span class="drawer-title">Navegación</span>
    <button popovertarget="mobile-nav"
            popovertargetaction="hide"
            aria-label="Cerrar navegación">
      &times;
    </button>
  </div>
  <a href="/" class="drawer-link">Inicio</a>
  <a href="/products" class="drawer-link">Productos</a>
  <a href="/about" class="drawer-link">Nosotros</a>
  <a href="/contact" class="drawer-link">Contacto</a>
</nav>

<style>
  .drawer {
    margin: 0;
    padding: 0;
    position: fixed;
    inset: 0 auto 0 0;
    width: min(85vw, 320px);
    height: 100vh;
    border: none;
    background: #1a1a2e;
    transform: translateX(-100%);
    transition: transform 0.3s ease;
  }

  .drawer:popover-open {
    transform: translateX(0);
  }

  .drawer::backdrop {
    background: rgba(0,0,0,0.5);
    opacity: 0;
    transition: opacity 0.3s;
  }

  .drawer:popover-open::backdrop {
    opacity: 1;
  }
</style>

Usamos popover="manual" aquí porque queremos control explícito sobre cuándo se cierra - los usuarios en móvil esperan tocar un botón de cierre o un enlace de navegación, no que el drawer desaparezca cuando tocan accidentalmente cerca del borde.

Soporte de Navegadores para Popover

La Popover API está soportada en todos los navegadores modernos desde Abril 2024. Verifica el estado actual de compatibilidad:

Can I Use: Popover API - Tabla interactiva de soporte de navegadores

Para navegadores más antiguos, trata el popover como mejora progresiva: proporciona un fallback que muestre el contenido inline o usa un polyfill.


Details y Summary: Acordeones Nativos Sin Complicaciones

Referencia Rápida: Elementos Details/Summary

Elemento/Atributo Propósito Ejemplo
<details> Contenedor para contenido colapsable <details>...</details>
<summary> Header/etiqueta clicable <summary>Haz clic aquí</summary>
atributo open Pre-expande la sección <details open>
atributo name Crea grupos de acordeón exclusivos <details name="faq">
evento toggle Se dispara cuando se abre/cierra details.addEventListener('toggle', ...)

Soporte de Navegadores: Details/Summary 97%+ global; atributo name 85%+ (Chrome 120+, Firefox 130+, Safari 17.2+)

Los elementos <details> y <summary> crean widgets de disclosure - componentes UI que se expanden y colapsan para mostrar u ocultar contenido. Son perfectos para FAQs, secciones expandibles, sidebars colapsables y sí, acordeones.

El Problema con los Acordeones Personalizados

Una típica implementación de acordeón en JavaScript implica:

// Componente Acordeón - la versión "simple"
class Accordion {
  constructor(container) {
    this.container = container;
    this.panels = container.querySelectorAll('.panel');
    this.bindEvents();
  }

  bindEvents() {
    this.panels.forEach(panel => {
      const header = panel.querySelector('.header');
      header.addEventListener('click', () => this.toggle(panel));
      header.addEventListener('keydown', (e) => {
        if (e.key === 'Enter' || e.key === ' ') {
          e.preventDefault();
          this.toggle(panel);
        }
      });
    });
  }

  toggle(panel) {
    const isOpen = panel.classList.contains('open');

    // Cerrar todos los paneles (para acordeón exclusivo)
    this.panels.forEach(p => {
      p.classList.remove('open');
      p.querySelector('.header').setAttribute('aria-expanded', 'false');
      p.querySelector('.content').setAttribute('aria-hidden', 'true');
    });

    // Abrir el panel clicado si estaba cerrado
    if (!isOpen) {
      panel.classList.add('open');
      panel.querySelector('.header').setAttribute('aria-expanded', 'true');
      panel.querySelector('.content').setAttribute('aria-hidden', 'false');
    }
  }
}

Además necesitas la estructura HTML correcta con todos los atributos ARIA, CSS para animaciones y atención cuidadosa a los requisitos de accesibilidad. Es mucho código para un simple comportamiento de “expandir/colapsar”.

La Solución Nativa

<details>
  <summary>¿Cuál es su política de devoluciones?</summary>
  <p>Ofrecemos una política de devolución de 30 días en todos los artículos. Los artículos deben estar
  en condición original con etiquetas adjuntas.</p>
</details>

El navegador maneja todo:

  • Clic para expandir/colapsar
  • Accesibilidad por teclado (Enter y Space hacen toggle)
  • Roles ARIA correctos (implícitamente)
  • Anuncios de lector de pantalla
  • Gestión del foco

Prueba la demo en vivo en CodePen

Creando Acordeones Exclusivos con el Atributo name

El atributo name es una adición relativamente reciente que habilita los acordeones exclusivos - donde abrir una sección cierra automáticamente las otras:

<section class="faq" aria-label="Preguntas Frecuentes">
  <details name="faq-group" open>
    <summary>¿Cómo creo una cuenta?</summary>
    <div class="answer">
      <p>Haz clic en el botón "Registrarse" en la esquina superior derecha.
      Ingresa tu email y elige una contraseña.
      Recibirás un email de confirmación en minutos.</p>
    </div>
  </details>

  <details name="faq-group">
    <summary>¿Cómo restablezco mi contraseña?</summary>
    <div class="answer">
      <p>Haz clic en "Olvidé mi contraseña" en la página de inicio de sesión.
      Ingresa tu dirección de email y te enviaremos un enlace de restablecimiento.
      El enlace expira después de 24 horas.</p>
    </div>
  </details>

  <details name="faq-group">
    <summary>¿Puedo cambiar mi nombre de usuario?</summary>
    <div class="answer">
      <p>¡Sí! Ve a Configuración > Perfil > Editar Nombre de Usuario.
      Nota que solo puedes cambiar tu nombre de usuario una vez cada 30 días.</p>
    </div>
  </details>

  <details name="faq-group">
    <summary>¿Cómo contacto soporte?</summary>
    <div class="answer">
      <p>Puedes contactar a nuestro equipo de soporte vía email a support@example.com
      o a través del widget de chat en vivo disponible en cada página.
      Típicamente respondemos dentro de 2 horas durante horario laboral.</p>
    </div>
  </details>
</section>

Todos los elementos <details> con el mismo valor name forman un grupo. Cuando abres uno, el navegador cierra automáticamente los otros. Sin JavaScript requerido.

Prueba la demo en vivo en CodePen

Estilizando Elementos Details

El estilo por defecto es funcional pero simple. Aquí hay cómo crear un acordeón pulido:

.faq {
  max-width: 700px;
  margin: 2rem auto;
}

.faq details {
  border: 1px solid #e0e0e0;
  border-radius: 8px;
  margin-bottom: 0.75rem;
  background: #fff;
  overflow: hidden;
}

.faq summary {
  padding: 1rem 1.25rem;
  font-weight: 600;
  cursor: pointer;
  list-style: none; /* Eliminar marcador por defecto */
  display: flex;
  justify-content: space-between;
  align-items: center;
  transition: background 0.2s;
}

.faq summary:hover {
  background: #f5f5f5;
}

/* Eliminar marcador por defecto en Safari */
.faq summary::-webkit-details-marker {
  display: none;
}

/* Indicador expandir/colapsar personalizado */
.faq summary::after {
  content: '+';
  font-size: 1.5rem;
  font-weight: 300;
  color: #666;
  transition: transform 0.2s;
}

.faq details[open] summary::after {
  content: '−';
}

.faq details[open] summary {
  border-bottom: 1px solid #e0e0e0;
}

.faq .answer {
  padding: 1rem 1.25rem;
  line-height: 1.6;
  color: #444;
}

Añadiendo Animaciones Suaves

Los elementos <details> nativos no se animan por defecto, pero puedes añadir transiciones suaves con un poco de CSS:

.faq details {
  /* ... otros estilos ... */
}

.faq .answer {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows 0.3s ease;
}

.faq details[open] .answer {
  grid-template-rows: 1fr;
}

.faq .answer > * {
  overflow: hidden;
}

Esto usa la técnica CSS Grid para animar altura de 0 a auto - algo que tradicionalmente ha sido imposible con solo CSS.

Manejando el Evento toggle

Cuando necesitas responder a eventos de abrir/cerrar:

const details = document.querySelector('details');

details.addEventListener('toggle', (event) => {
  if (details.open) {
    console.log('Panel abierto');
    // Rastrear analytics, cargar contenido, etc.
  } else {
    console.log('Panel cerrado');
  }
});

Snippets de Código Colapsables

Details/Summary es excelente para mostrar ejemplos de código que los usuarios pueden expandir cuando lo necesiten:

<article class="tutorial">
  <h3>Conectando a la Base de Datos</h3>
  <p>Primero, instala el paquete requerido y configura tu string de conexión.</p>

  <details class="code-block">
    <summary>
      <span class="code-label">database.js</span>
      <span class="code-lang">JavaScript</span>
    </summary>
    <pre><code>const { Pool } = require('pg');

const pool = new Pool({
  host: process.env.DB_HOST,
  port: process.env.DB_PORT || 5432,
  database: process.env.DB_NAME,
  user: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
  ssl: process.env.NODE_ENV === 'production'
});

module.exports = { pool };</code></pre>
  </details>
</article>

<style>
  .code-block {
    margin: 1rem 0;
    border: 1px solid #e1e4e8;
    border-radius: 6px;
    overflow: hidden;
  }

  .code-block summary {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 0.75rem 1rem;
    background: #f6f8fa;
    cursor: pointer;
    list-style: none;
  }

  .code-block summary::-webkit-details-marker {
    display: none;
  }

  .code-label {
    font-family: monospace;
    font-weight: 600;
  }

  .code-lang {
    font-size: 0.75rem;
    padding: 0.25rem 0.5rem;
    background: #e1e4e8;
    border-radius: 4px;
  }

  .code-block pre {
    margin: 0;
    padding: 1rem;
    background: #24292e;
    color: #e1e4e8;
    overflow-x: auto;
  }
</style>

Prueba la demo en vivo en CodePen

Details Anidados para Contenido Jerárquico

Puedes anidar elementos <details> para crear estructuras tipo árbol para documentación o navegación:

<nav class="docs-nav" aria-label="Documentación">
  <details open>
    <summary>Primeros Pasos</summary>
    <ul>
      <li><a href="/docs/installation">Instalación</a></li>
      <li><a href="/docs/quickstart">Inicio Rápido</a></li>
    </ul>
  </details>

  <details>
    <summary>Conceptos Fundamentales</summary>
    <ul>
      <li><a href="/docs/components">Componentes</a></li>
      <li>
        <details>
          <summary>Gestión de Estado</summary>
          <ul>
            <li><a href="/docs/state/local">Estado Local</a></li>
            <li><a href="/docs/state/global">Estado Global</a></li>
            <li><a href="/docs/state/async">Estado Asíncrono</a></li>
          </ul>
        </details>
      </li>
      <li><a href="/docs/routing">Enrutamiento</a></li>
    </ul>
  </details>

  <details>
    <summary>Referencia API</summary>
    <ul>
      <li><a href="/api/hooks">Hooks</a></li>
      <li><a href="/api/utilities">Utilidades</a></li>
    </ul>
  </details>
</nav>

Prueba la demo en vivo en CodePen

Este patrón es particularmente útil para sidebars de documentación, exploradores de archivos o cualquier dato jerárquico que se beneficie de la revelación progresiva.

Ejemplo Práctico: Deep-Linking a Secciones Abiertas

Un requisito común es enlazar directamente a un panel de acordeón abierto:

<details id="shipping" name="faq">
  <summary>Información de Envío</summary>
  <p>Enviamos a todo el mundo. El envío estándar toma 5-7 días hábiles.</p>
</details>

<details id="returns" name="faq">
  <summary>Devoluciones y Reembolsos</summary>
  <p>Política de devolución de 30 días en todos los artículos.</p>
</details>

<script>
  // Abrir la sección especificada en el hash de la URL
  function openFromHash() {
    const hash = window.location.hash.slice(1);
    if (hash) {
      const target = document.getElementById(hash);
      if (target && target.tagName === 'DETAILS') {
        target.open = true;
        target.scrollIntoView({ behavior: 'smooth' });
      }
    }
  }

  // Ejecutar al cargar la página
  openFromHash();

  // Actualizar URL cuando las secciones hacen toggle
  document.querySelectorAll('details[name="faq"]').forEach(details => {
    details.addEventListener('toggle', () => {
      if (details.open) {
        history.replaceState(null, '', `#${details.id}`);
      }
    });
  });
</script>

Ahora los usuarios pueden compartir URLs como https://example.com/faq#returns que abren directamente en la sección relevante.

Soporte de Navegadores para Details/Summary

Los elementos <details> y <summary> han sido ampliamente soportados desde 2020. El atributo name para acordeones exclusivos es más nuevo - soportado en Chrome 120+, Safari 17.2+ y Firefox 130+. Para navegadores más antiguos, el acordeón seguirá funcionando, pero múltiples paneles podrán estar abiertos simultáneamente.

Verifica el estado actual de compatibilidad:


Datalist: Autocompletado Nativo Sin Dependencias

Referencia Rápida: Elemento Datalist

Funcionalidad Sintaxis Descripción
Uso básico <input list="id"> + <datalist id="id"> Vincula input a sugerencias
Opciones <option value="..."> Cada sugerencia
Con etiquetas <option value="val" label="Mostrar"> Muestra etiqueta, envía valor
Tipos de input text, email, url, tel, number, range, color, date, time Todos soportan datalist

Diferencia clave con <select>: Los usuarios pueden escribir valores personalizados no incluidos en la lista

Soporte de Navegadores: 96%+ global (todos los navegadores modernos)

El elemento <datalist> proporciona una lista de valores sugeridos para un campo input. Es la solución nativa para dropdowns de autocompletado, sugerencias type-ahead y patrones de combobox.

El Problema con el Autocompletado Personalizado

Las implementaciones de autocompletado son notoriamente complejas:

// Un autocompletado "simple" - abreviado
class Autocomplete {
  constructor(input, options) {
    this.input = input;
    this.options = options;
    this.dropdown = this.createDropdown();
    this.selectedIndex = -1;
    this.bindEvents();
  }

  createDropdown() {
    const dropdown = document.createElement('div');
    dropdown.className = 'autocomplete-dropdown';
    dropdown.setAttribute('role', 'listbox');
    this.input.parentNode.appendChild(dropdown);
    return dropdown;
  }

  bindEvents() {
    this.input.addEventListener('input', () => this.onInput());
    this.input.addEventListener('keydown', (e) => this.onKeydown(e));
    this.input.addEventListener('blur', () => this.hideDropdown());
    document.addEventListener('click', (e) => this.onOutsideClick(e));
  }

  onInput() {
    const value = this.input.value.toLowerCase();
    const matches = this.options.filter(opt =>
      opt.toLowerCase().includes(value)
    );
    this.renderDropdown(matches);
  }

  onKeydown(e) {
    switch(e.key) {
      case 'ArrowDown':
        e.preventDefault();
        this.selectNext();
        break;
      case 'ArrowUp':
        e.preventDefault();
        this.selectPrevious();
        break;
      case 'Enter':
        e.preventDefault();
        this.confirmSelection();
        break;
      case 'Escape':
        this.hideDropdown();
        break;
    }
  }

  // ... muchos más métodos para rendering, selección, posicionamiento, etc.
}

Las implementaciones personalizadas necesitan manejar navegación por teclado, gestión del foco, posicionamiento, scrolling dentro del dropdown, anuncios de lector de pantalla y mucho más.

La Solución Nativa

<label for="country">País:</label>
<input type="text" id="country" name="country" list="countries">

<datalist id="countries">
  <option value="Argentina">
  <option value="Australia">
  <option value="Austria">
  <option value="Bélgica">
  <option value="Brasil">
  <option value="Canadá">
  <option value="España">
  <option value="Francia">
  <option value="Alemania">
  <option value="Italia">
  <option value="Japón">
  <option value="México">
  <option value="Reino Unido">
  <option value="Estados Unidos">
</datalist>

Esa es la implementación completa. El navegador proporciona:

  • Un dropdown con sugerencias coincidentes
  • Navegación por teclado (teclas de flecha, Enter para seleccionar)
  • Filtrado mientras el usuario escribe
  • Accesibilidad nativa
  • UI optimizada para móvil

Prueba la demo en vivo en CodePen

Características Clave de Datalist

A diferencia de <select>, el enfoque datalist:

  1. Permite entrada libre: Los usuarios pueden escribir cualquier cosa, no solo las opciones predefinidas
  2. Muestra sugerencias basadas en la entrada: La lista filtra mientras el usuario escribe
  3. Es mejora progresiva: Si datalist no está soportado, el input sigue funcionando como campo de texto normal

Usando Datalist con Diferentes Tipos de Input

Datalist funciona con muchos tipos de input, no solo text:

Sugerencias de email:

<input type="email" list="email-domains">
<datalist id="email-domains">
  <option value="@gmail.com">
  <option value="@outlook.com">
  <option value="@yahoo.com">
  <option value="@icloud.com">
</datalist>

Sugerencias de URL:

<input type="url" list="common-urls">
<datalist id="common-urls">
  <option value="https://github.com/">
  <option value="https://stackoverflow.com/">
  <option value="https://developer.mozilla.org/">
</datalist>

Input range con marcas:

<label>
  Calificación:
  <input type="range" min="0" max="10" list="ratings">
</label>
<datalist id="ratings">
  <option value="0" label="Malo">
  <option value="5" label="Promedio">
  <option value="10" label="Excelente">
</datalist>

Selector de color con presets:

<label>
  Color de Marca:
  <input type="color" list="brand-colors">
</label>
<datalist id="brand-colors">
  <option value="#FF5733" label="Naranja Atardecer">
  <option value="#3498DB" label="Azul Océano">
  <option value="#2ECC71" label="Verde Esmeralda">
  <option value="#9B59B6" label="Púrpura Real">
  <option value="#F39C12" label="Amarillo Dorado">
</datalist>

Prueba la demo en vivo en CodePen

Caja de Búsqueda con Búsquedas Recientes

Un patrón común es mostrar el historial de búsquedas recientes como sugerencias:

<div class="search-container">
  <input type="search"
         id="search-input"
         list="recent-searches"
         placeholder="Buscar productos..."
         autocomplete="off">
  <datalist id="recent-searches">
    <!-- Poblado desde localStorage o API -->
  </datalist>
</div>

<script>
  const searchInput = document.getElementById('search-input');
  const datalist = document.getElementById('recent-searches');

  // Cargar búsquedas recientes desde localStorage
  function loadRecentSearches() {
    const searches = JSON.parse(localStorage.getItem('recentSearches') || '[]');
    datalist.innerHTML = searches
      .slice(0, 5) // Mostrar últimas 5 búsquedas
      .map(s => `<option value="${s}">`)
      .join('');
  }

  // Guardar búsqueda cuando el usuario envía
  function saveSearch(query) {
    if (!query.trim()) return;

    let searches = JSON.parse(localStorage.getItem('recentSearches') || '[]');
    searches = [query, ...searches.filter(s => s !== query)].slice(0, 10);
    localStorage.setItem('recentSearches', JSON.stringify(searches));
    loadRecentSearches();
  }

  loadRecentSearches();

  // Guardar en tecla Enter o envío de formulario
  searchInput.addEventListener('keydown', (e) => {
    if (e.key === 'Enter') {
      saveSearch(searchInput.value);
    }
  });
</script>

Prueba la demo en vivo en CodePen

Selector de Hora con Horarios Comunes

Datalist funciona genial para inputs de tiempo con opciones preset:

<label for="meeting-time">Programar reunión:</label>
<input type="time"
       id="meeting-time"
       list="common-times"
       min="09:00"
       max="18:00">

<datalist id="common-times">
  <option value="09:00" label="9:00 AM">
  <option value="09:30" label="9:30 AM">
  <option value="10:00" label="10:00 AM">
  <option value="10:30" label="10:30 AM">
  <option value="11:00" label="11:00 AM">
  <option value="11:30" label="11:30 AM">
  <option value="12:00" label="12:00 PM - Mediodía">
  <option value="13:00" label="1:00 PM">
  <option value="14:00" label="2:00 PM">
  <option value="15:00" label="3:00 PM">
  <option value="16:00" label="4:00 PM">
  <option value="17:00" label="5:00 PM">
</datalist>

Prueba la demo en vivo en CodePen

Input Numérico con Valores Sugeridos

Para inputs numéricos donde ciertos valores son comunes:

<label for="quantity">Cantidad:</label>
<input type="number"
       id="quantity"
       list="common-quantities"
       min="1"
       max="1000"
       value="1">

<datalist id="common-quantities">
  <option value="1">
  <option value="5">
  <option value="10">
  <option value="25">
  <option value="50">
  <option value="100">
</datalist>

Prueba la demo en vivo en CodePen

Datalist Dinámico con JavaScript

Para autocompletado que necesita obtener sugerencias de una API:

<input type="text"
       id="search"
       list="search-suggestions"
       placeholder="Buscar productos...">
<datalist id="search-suggestions"></datalist>

<script>
  const input = document.getElementById('search');
  const datalist = document.getElementById('search-suggestions');
  let debounceTimer;

  input.addEventListener('input', () => {
    clearTimeout(debounceTimer);

    if (input.value.length < 2) {
      datalist.innerHTML = '';
      return;
    }

    debounceTimer = setTimeout(async () => {
      try {
        const response = await fetch(
          `/api/suggestions?q=${encodeURIComponent(input.value)}`
        );
        const suggestions = await response.json();

        datalist.innerHTML = suggestions
          .map(s => `<option value="${escapeHtml(s)}">`)
          .join('');
      } catch (error) {
        console.error('Error al obtener sugerencias:', error);
      }
    }, 300);
  });

  function escapeHtml(text) {
    const div = document.createElement('div');
    div.textContent = text;
    return div.innerHTML;
  }
</script>

Cuándo Usar Datalist vs. Autocompletado Personalizado

Usa datalist cuando:

  • Necesitas sugerencias simples de una lista predefinida o dinámica
  • La personalización visual no es crítica
  • Quieres máxima accesibilidad con código mínimo
  • La lista de opciones es de tamaño razonable (decenas, no miles)

Usa un autocompletado personalizado cuando:

  • Necesitas contenido enriquecido en las sugerencias (imágenes, descripciones, categorías)
  • Requieres control fino sobre el estilo
  • Estás construyendo una caja de búsqueda con funcionalidades avanzadas (resaltado, agrupación)
  • Necesitas manejar conjuntos de datos muy grandes con virtualización

Consideraciones de Accesibilidad

Mientras que datalist es generalmente accesible, ten en cuenta algunas limitaciones:

  • La apariencia del dropdown es controlada por el navegador y no se puede estilizar completamente
  • Algunas combinaciones de lector de pantalla + navegador no anuncian las sugerencias perfectamente
  • El tamaño de fuente en el dropdown no escala con el zoom de la página

Para funcionalidad crítica, siempre prueba con tu audiencia objetivo y tecnologías asistivas.

Soporte de Navegadores para Datalist

El elemento <datalist> está bien soportado en todos los navegadores modernos. Verifica el estado actual de compatibilidad:

Can I Use: Datalist element - Tabla interactiva de soporte de navegadores (96%+ soporte global)

Nota que mientras el soporte básico es excelente, el comportamiento puede variar ligeramente entre navegadores - particularmente en dispositivos móviles. Siempre prueba tu caso de uso específico.


Combinando Estas Funcionalidades: Un Ejemplo Completo

Construyamos un ejemplo práctico que combina las tres funcionalidades - un panel de configuración con popover, secciones de acordeón e inputs de autocompletado:

<!DOCTYPE html>
<html lang="es">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Panel de Configuración - Funcionalidades HTML Nativas</title>
  <style>
    * {
      box-sizing: border-box;
      margin: 0;
      padding: 0;
    }

    body {
      font-family: system-ui, -apple-system, sans-serif;
      background: #f5f5f7;
      min-height: 100vh;
      padding: 2rem;
    }

    .container {
      max-width: 600px;
      margin: 0 auto;
    }

    h1 {
      margin-bottom: 1.5rem;
      color: #1d1d1f;
    }

    /* Botón Configuración */
    .settings-btn {
      display: inline-flex;
      align-items: center;
      gap: 0.5rem;
      padding: 0.75rem 1.25rem;
      background: #007aff;
      color: white;
      border: none;
      border-radius: 8px;
      font-size: 1rem;
      cursor: pointer;
      transition: background 0.2s;
    }

    .settings-btn:hover {
      background: #0056b3;
    }

    /* Panel de Configuración (Popover) */
    .settings-panel {
      margin: 0;
      padding: 1.5rem;
      width: min(90vw, 500px);
      border: none;
      border-radius: 16px;
      background: white;
      box-shadow: 0 20px 60px rgba(0,0,0,0.15);
    }

    .settings-panel::backdrop {
      background: rgba(0,0,0,0.3);
    }

    .panel-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      margin-bottom: 1.5rem;
      padding-bottom: 1rem;
      border-bottom: 1px solid #e5e5e5;
    }

    .panel-header h2 {
      font-size: 1.25rem;
      color: #1d1d1f;
    }

    .close-btn {
      width: 32px;
      height: 32px;
      border: none;
      border-radius: 50%;
      background: #f5f5f5;
      font-size: 1.25rem;
      cursor: pointer;
      transition: background 0.2s;
    }

    .close-btn:hover {
      background: #e5e5e5;
    }

    /* Secciones de Acordeón */
    .settings-section {
      border: 1px solid #e5e5e5;
      border-radius: 12px;
      margin-bottom: 0.75rem;
      overflow: hidden;
    }

    .settings-section summary {
      padding: 1rem;
      font-weight: 600;
      cursor: pointer;
      list-style: none;
      display: flex;
      justify-content: space-between;
      align-items: center;
      background: #fafafa;
      transition: background 0.2s;
    }

    .settings-section summary:hover {
      background: #f0f0f0;
    }

    .settings-section summary::-webkit-details-marker {
      display: none;
    }

    .settings-section summary::after {
      content: '▸';
      transition: transform 0.2s;
    }

    .settings-section[open] summary::after {
      transform: rotate(90deg);
    }

    .settings-section[open] summary {
      border-bottom: 1px solid #e5e5e5;
    }

    .section-content {
      padding: 1rem;
    }

    /* Estilos de Formulario */
    .form-group {
      margin-bottom: 1rem;
    }

    .form-group:last-child {
      margin-bottom: 0;
    }

    .form-group label {
      display: block;
      margin-bottom: 0.5rem;
      font-size: 0.875rem;
      font-weight: 500;
      color: #666;
    }

    .form-group input,
    .form-group select {
      width: 100%;
      padding: 0.75rem;
      border: 1px solid #ddd;
      border-radius: 8px;
      font-size: 1rem;
      transition: border-color 0.2s;
    }

    .form-group input:focus,
    .form-group select:focus {
      outline: none;
      border-color: #007aff;
    }

    /* Botón Guardar */
    .save-btn {
      width: 100%;
      padding: 1rem;
      margin-top: 1rem;
      background: #007aff;
      color: white;
      border: none;
      border-radius: 8px;
      font-size: 1rem;
      font-weight: 600;
      cursor: pointer;
      transition: background 0.2s;
    }

    .save-btn:hover {
      background: #0056b3;
    }
  </style>
</head>
<body>
  <div class="container">
    <h1>Panel de Control</h1>

    <button class="settings-btn" popovertarget="settings-panel">
      <span>&#9881;</span> Configuración
    </button>
  </div>

  <!-- Popover Panel de Configuración -->
  <div id="settings-panel" popover class="settings-panel">
    <div class="panel-header">
      <h2>Configuración</h2>
      <button class="close-btn"
              popovertarget="settings-panel"
              popovertargetaction="hide"
              aria-label="Cerrar configuración">
        &times;
      </button>
    </div>

    <form id="settings-form">
      <!-- Sección Perfil -->
      <details class="settings-section" name="settings" open>
        <summary>Perfil</summary>
        <div class="section-content">
          <div class="form-group">
            <label for="display-name">Nombre para Mostrar</label>
            <input type="text" id="display-name" name="displayName"
                   value="Juan García">
          </div>
          <div class="form-group">
            <label for="timezone">Zona Horaria</label>
            <input type="text" id="timezone" name="timezone"
                   list="timezones" placeholder="Comienza a escribir...">
            <datalist id="timezones">
              <option value="Europe/Madrid">
              <option value="Europe/London">
              <option value="Europe/Paris">
              <option value="America/New_York">
              <option value="America/Los_Angeles">
              <option value="America/Mexico_City">
              <option value="America/Buenos_Aires">
              <option value="Asia/Tokyo">
              <option value="Australia/Sydney">
            </datalist>
          </div>
        </div>
      </details>

      <!-- Sección Notificaciones -->
      <details class="settings-section" name="settings">
        <summary>Notificaciones</summary>
        <div class="section-content">
          <div class="form-group">
            <label for="email-frequency">Frecuencia de Email</label>
            <select id="email-frequency" name="emailFrequency">
              <option value="immediate">Inmediato</option>
              <option value="daily">Resumen Diario</option>
              <option value="weekly">Resumen Semanal</option>
              <option value="never">Nunca</option>
            </select>
          </div>
        </div>
      </details>

      <!-- Sección Apariencia -->
      <details class="settings-section" name="settings">
        <summary>Apariencia</summary>
        <div class="section-content">
          <div class="form-group">
            <label for="language">Idioma</label>
            <input type="text" id="language" name="language"
                   list="languages" value="Español">
            <datalist id="languages">
              <option value="Español">
              <option value="English">
              <option value="Italiano">
              <option value="Français">
              <option value="Deutsch">
              <option value="Português">
              <option value="日本語">
              <option value="中文">
            </datalist>
          </div>
          <div class="form-group">
            <label for="theme-color">Color de Acento</label>
            <input type="color" id="theme-color" name="themeColor"
                   list="theme-colors" value="#007aff">
            <datalist id="theme-colors">
              <option value="#007aff" label="Azul">
              <option value="#34c759" label="Verde">
              <option value="#ff9500" label="Naranja">
              <option value="#ff2d55" label="Rosa">
              <option value="#5856d6" label="Púrpura">
            </datalist>
          </div>
        </div>
      </details>

      <button type="submit" class="save-btn">Guardar Cambios</button>
    </form>
  </div>

  <script>
    // Manejar envío de formulario
    document.getElementById('settings-form').addEventListener('submit', (e) => {
      e.preventDefault();

      const formData = new FormData(e.target);
      const settings = Object.fromEntries(formData);

      console.log('Guardando configuración:', settings);

      // Cerrar el panel después de guardar
      document.getElementById('settings-panel').hidePopover();

      // Mostrar feedback (en producción, usa una notificación toast)
      alert('¡Configuración guardada exitosamente!');
    });
  </script>
</body>
</html>

Este ejemplo demuestra:

  • Un popover para el panel de configuración que maneja mostrar/ocultar, backdrop y navegación por teclado
  • Details/Summary con el atributo name para secciones de acordeón exclusivas
  • Datalist para sugerencias de zona horaria, idioma y color
  • JavaScript mínimo - solo para el manejo del envío del formulario

Cuando el HTML Nativo No Es Suficiente

A pesar del poder de estas funcionalidades nativas, hay casos legítimos donde las soluciones JavaScript personalizadas tienen sentido:

Limitaciones de Popover:

  • Requisitos de posicionamiento complejos (el anchor positioning aún es experimental)
  • Popovers anidados con patrones de interacción complejos
  • Popovers que necesitan sobrevivir a la navegación en single-page apps

Limitaciones de Details/Summary:

  • Acordeones que necesitan soportar múltiples paneles abiertos con animaciones
  • Animaciones complejas de abrir/cerrar más allá de simples transiciones
  • Acordeones anidados con estado compartido

Limitaciones de Datalist:

  • Renderizado enriquecido de sugerencias (imágenes, descripciones, categorías)
  • Conjuntos de datos grandes que requieren virtualización
  • Lógica de filtrado personalizada o coincidencia difusa
  • Patrones de combobox completos con funcionalidad de crear-nuevo

En estos casos, recurre a bibliotecas establecidas como Radix UI, Headless UI o Downshift. Pero siempre pregúntate primero: “¿El navegador ya hace esto?”


Conclusión

La plataforma del navegador ha recorrido un largo camino. Funcionalidades que una vez requerían cientos de líneas de JavaScript y dependencias de terceros ahora están disponibles como atributos y elementos HTML nativos.

La Popover API nos da menús desplegables, tooltips y paneles deslizantes con correcta gestión del foco y accesibilidad integrada. Los elementos Details y Summary proporcionan acordeones que funcionan correctamente desde el inicio, con el atributo name habilitando comportamiento exclusivo. El elemento Datalist ofrece sugerencias de autocompletado sin la complejidad de implementaciones personalizadas.

Al usar estas funcionalidades nativas, tú:

  • Reduces la complejidad del código - menos código significa menos bugs
  • Mejoras la accesibilidad - las implementaciones del navegador siguen estándares
  • Mejoras el rendimiento - las funcionalidades nativas están optimizadas a nivel de motor
  • Simplificas el mantenimiento - el equipo del navegador maneja los casos límite

La filosofía es simple: deja que el navegador maneje el comportamiento que ya sabe hacer. Reserva tu JavaScript para la lógica de producto que hace única a tu aplicación.

Comienza con HTML nativo. Recurre a JavaScript solo cuando hayas agotado lo que la plataforma proporciona. Tus usuarios, tu equipo y tu yo futuro te lo agradecerán.


Preguntas Frecuentes (FAQ)

¿Qué es la Popover API de HTML?

La Popover API es una funcionalidad HTML nativa que te permite crear elementos de overlay (como menús desplegables, tooltips y diálogos) usando simples atributos HTML. Al añadir popover a un elemento y popovertarget a un botón, el navegador maneja automáticamente mostrar/ocultar el elemento, cierre al clic externo, navegación por teclado (Escape para cerrar), gestión del foco y apilamiento de z-index. Está soportada en todos los navegadores modernos desde 2024.

¿Cómo creo un menú desplegable sin JavaScript?

Usa la Popover API:

<button popovertarget="menu">Abrir Menú</button>
<div id="menu" popover>
  <a href="#">Opción 1</a>
  <a href="#">Opción 2</a>
</div>

El menú aparecerá cuando hagas clic en el botón y se cerrará automáticamente cuando hagas clic fuera o presiones Escape.

¿Cuál es la diferencia entre popover=“auto” y popover=“manual”?

  • popover="auto" (por defecto): El popover se cierra automáticamente cuando haces clic fuera o presionas Escape. Solo un popover auto puede estar abierto a la vez.
  • popover="manual": El popover solo se cierra cuando se activa explícitamente vía JavaScript u otro botón con popovertargetaction="hide". Múltiples popovers manuales pueden estar abiertos simultáneamente.

¿Cómo creo un acordeón que solo permite una sección abierta a la vez?

Usa el atributo name en los elementos <details>:

<details name="faq" open>
  <summary>Pregunta 1</summary>
  <p>Respuesta 1</p>
</details>
<details name="faq">
  <summary>Pregunta 2</summary>
  <p>Respuesta 2</p>
</details>

Todos los elementos <details> con el mismo valor name forman un grupo exclusivo - abrir uno cierra automáticamente los otros.

¿Para qué se usa el elemento datalist?

El elemento <datalist> proporciona sugerencias de autocompletado para campos input. A diferencia de <select>, los usuarios todavía pueden escribir valores personalizados - las sugerencias son pistas opcionales. Funciona con varios tipos de input incluyendo text, email, number, range y color.

<input type="text" list="fruits">
<datalist id="fruits">
  <option value="Manzana">
  <option value="Plátano">
  <option value="Cereza">
</datalist>

¿La Popover API reemplaza al elemento dialog?

No, sirven para propósitos diferentes:

  • Popover: Para overlays no modales como menús, tooltips y notificaciones. Los usuarios todavía pueden interactuar con la página.
  • Dialog: Para interacciones modales que requieren atención del usuario antes de continuar. Usa <dialog> con showModal() para formularios, confirmaciones y alertas críticas.

¿Qué es CSS Anchor Positioning?

CSS Anchor Positioning es una funcionalidad CSS que te permite posicionar elementos relativos a un elemento “ancla”. Es particularmente útil con popovers para posicionar menús desplegables directamente debajo de sus botones de activación:

.button { anchor-name: --my-anchor; }
.popover {
  position-anchor: --my-anchor;
  top: anchor(bottom);
  left: anchor(left);
}

Está soportado en Chrome 125+ y otros navegadores modernos.

¿Son accesibles estas funcionalidades HTML?

Sí, estas funcionalidades HTML nativas están construidas con la accesibilidad en mente:

  • Popover: Correcta gestión del foco, navegación por teclado y atributos ARIA son manejados automáticamente
  • Details/Summary: Los lectores de pantalla anuncian el estado expandido/colapsado
  • Datalist: Funciona con lectores de pantalla y navegación por teclado

Sin embargo, siempre prueba con tu audiencia objetivo y tecnologías asistivas para funcionalidad crítica.

¿Qué navegadores soportan Popover, Details y Datalist?

Funcionalidad Chrome Firefox Safari Edge Soporte Global
Popover API 114+ 125+ 17+ 114+ ~90%
Details/Summary 12+ 49+ 6+ 79+ ~97%
Details name attribute 120+ 130+ 17.2+ 120+ ~85%
Datalist 20+ 4+ 12.1+ 12+ ~96%

Verifica caniuse.com para los datos más actualizados de soporte de navegadores.

¿Cuándo debería seguir usando JavaScript para componentes UI?

Usa JavaScript cuando necesites:

  • Contenido enriquecido en opciones de dropdown (imágenes, descripciones)
  • Animaciones complejas más allá de transiciones CSS
  • Conjuntos de datos muy grandes que requieren virtualización
  • Lógica de filtrado personalizada o búsqueda difusa
  • Patrones de combobox completos con funcionalidad de crear-nuevo
  • Popovers anidados con patrones de interacción complejos

Para la mayoría de los casos de uso comunes, las funcionalidades HTML nativas son suficientes y proporcionan mejor rendimiento y accesibilidad.


Fuentes y Referencias

Documentación Oficial

Tablas de Compatibilidad de Navegadores

Comments

comments powered by Disqus