wandres.dev
LA CAPA SUPERIOR · dialog, popover y el top layer

La API de invocación: command y commandfor

El sustituto declarativo de los manejadores de clic: comandos integrados para dialog y popover, comandos personalizados con CommandEvent, y su estado de soporte.

⏱ 17 min

popovertarget resolvió el caso de los popovers, pero dejó fuera a dialog: abrir un modal seguía exigiendo un manejador de clic. La API de invocación generaliza la idea con dos atributos, command y commandfor, que permiten declarar en el marcado qué hace un botón sobre qué elemento. Y como extra, deja la puerta abierta a comandos propios, con lo que el patrón sirve para cualquier componente y no solo para los dos que trae el HTML.

🎯 Al terminar esta lección sabrás
  • Abrir y cerrar un dialog sin escribir un manejador de clic.
  • Enumerar los comandos integrados y qué hace cada uno.
  • Definir comandos personalizados y escucharlos con CommandEvent.
  • Conocer el soporte real y escribir el respaldo.

Los dos atributos

commandfor apunta al id del elemento sobre el que se actúa. command dice qué acción se ejecuta. Los dos van en un button.

<button command="show-modal" commandfor="confirmar">Eliminar…</button>

<dialog id="confirmar">
  <p>¿Seguro?</p>
  <button command="close" commandfor="confirmar" value="cancelar">Cancelar</button>
  <button command="close" commandfor="confirmar" value="eliminar">Eliminar</button>
</dialog>

Ni una línea de JavaScript para la apertura y el cierre. El navegador se encarga, con la misma semántica que si hubieras llamado a showModal() y a close().

Los comandos integrados a agosto de 2026:

comando elemento efecto
show-modal dialog equivale a showModal()
close dialog equivale a close()
request-close dialog dispara cancel y cierra si nadie lo impide
show-popover elemento con popover equivale a showPopover()
hide-popover elemento con popover equivale a hidePopover()
toggle-popover elemento con popover equivale a togglePopover()

request-close merece un apunte porque es el que más se pasa por alto. close cierra sin más; request-close dispara el evento cancel antes de cerrar, exactamente igual que la tecla de escape, así que un manejador que impida el cierre cuando hay cambios sin guardar funciona también para ese botón. Es el comando correcto para un botón de “cerrar” que respeta la lógica de la aplicación.

Un botón con commandfor también establece ancla implícita sobre su objetivo, igual que popovertarget, lo que permite anclar el elemento invocado al botón sin declarar ningún nombre.

Comandos personalizados

Cualquier valor de command que empiece por dos guiones es un comando personalizado. El navegador no hace nada con él: se limita a disparar un evento CommandEvent de tipo command sobre el elemento destino.

<button command="--reproducir" commandfor="video-principal">Reproducir</button>
<button command="--pausar" commandfor="video-principal">Pausa</button>

<video id="video-principal" src="/demo.mp4"></video>
const video = document.querySelector('#video-principal');

video.addEventListener('command', (e) => {
  if (e.command === '--reproducir') video.play();
  if (e.command === '--pausar') video.pause();
});

El objeto del evento trae dos propiedades útiles: command, con la cadena del comando, y source, con el botón que lo disparó.

La ventaja frente a un addEventListener('click') por botón no es la brevedad, es la dirección de la dependencia. Con el patrón de clic, cada botón necesita conocer al componente y tener una referencia a él; el código de arranque tiene que encontrar los botones y engancharlos, y volver a hacerlo cuando se añadan más dinámicamente. Con comandos, el componente escucha su propio elemento y no conoce a ningún botón; cualquier botón en cualquier parte del documento puede invocarlo sin registro previo, incluido uno que llegue después por HTML dinámico. Es el mismo cambio de dirección que aporta la delegación de eventos, pero declarado en el marcado y con un vocabulario acotado.

flowchart TB
A[Un boton con command y commandfor] --> B{El comando empieza por dos guiones}
B -->|No, es integrado| C[El navegador ejecuta la accion sobre el destino]
C --> D[show-modal close request-close show-popover hide-popover toggle-popover]
B -->|Si, es personalizado| E[El navegador dispara CommandEvent sobre el destino]
E --> F[Tu componente escucha command en si mismo]
F --> G[Ningun boton necesita una referencia al componente]
style A fill:#89b4fa,color:#11111b
style C fill:#a6e3a1,color:#11111b
style E fill:#cba6f7,color:#11111b
style G fill:#a6e3a1,color:#11111b

El soporte y el respaldo

Esta es la parte con la que hay que tener cuidado, porque la API es genuinamente nueva.

command y commandfor llegaron a Chrome 135, a Firefox 144 y a Safari 26.2. El valor request-close llegó a Chrome algo más tarde, en la 139. El ancla implícita vía commandfor está en Chrome 135, Firefox 147 y Safari 26.2.

Es decir: está en los tres motores, pero solo desde versiones muy recientes. Si tu parque de usuarios incluye navegadores de 2025, necesitas un respaldo. La detección es directa:

if (!('command' in HTMLButtonElement.prototype)) {
  document.addEventListener('click', (e) => {
    const boton = e.target.closest('button[command][commandfor]');
    if (!boton) return;
    const destino = document.getElementById(boton.getAttribute('commandfor'));
    if (!destino) return;
    const cmd = boton.getAttribute('command');
    if (cmd === 'show-modal') destino.showModal();
    else if (cmd === 'close') destino.close(boton.value);
    else if (cmd === 'show-popover') destino.showPopover();
    else if (cmd === 'hide-popover') destino.hidePopover();
    else if (cmd === 'toggle-popover') destino.togglePopover();
    else if (cmd.startsWith('--')) {
      destino.dispatchEvent(
        Object.assign(new CustomEvent('command', { bubbles: true }),
                      { command: cmd, source: boton })
      );
    }
  });
}

Treinta líneas que puedes borrar dentro de un año. Comprobar 'command' in HTMLButtonElement.prototype es la detección correcta: la propiedad reflejada solo existe si el navegador implementa el atributo.

Conviene no confundir esta API con interestfor, un atributo experimental para mostrar contenido al pasar el ratón o enfocar. A agosto de 2026 solo está en Chrome 142, no es estándar todavía, y no debe usarse.

Los comandos devuelven al HTML una capacidad que perdió en 2005

Hay una lectura histórica de esta API que aclara por qué existe. El HTML original tenía exactamente un mecanismo declarativo de interacción: el formulario. Un form con su action y su method describía en el marcado qué hacía un botón, y el navegador lo ejecutaba. Cuando las aplicaciones se movieron al cliente, ese mecanismo dejó de servir —no había servidor al que enviar nada— y la industria lo sustituyó por manejadores de eventos en JavaScript. El coste de esa sustitución no fue la verbosidad, fue que el marcado dejó de describir el comportamiento. Un button en un HTML de 2015 no dice nada sobre lo que hace; hay que buscarlo en otro fichero, y a menudo hay que ejecutar la aplicación para saberlo. Con eso se perdieron tres cosas que el enfoque declarativo daba gratis: el comportamiento funcionaba antes de que el JavaScript cargara, funcionaba aunque el JavaScript fallara, y era inspeccionable leyendo el documento. popovertarget primero y command después reconstruyen ese mecanismo para el cliente, con un vocabulario acotado que el navegador entiende y una vía de escape —los comandos con doble guion— para lo que no está en el vocabulario. La consecuencia práctica que más se nota en un proyecto real es la última de las tres: volver a poder leer el comportamiento en el marcado. Cuando un menú deja de funcionar, mirar el HTML vuelve a ser un paso de diagnóstico útil.

⚔️ Sustituye los manejadores
  1. Abre un dialog modal con command="show-modal" y comprueba que no hace falta JavaScript.
  2. Compara command="close" con command="request-close" en un diálogo que impide el cierre desde el evento cancel.
  3. Define dos comandos personalizados sobre un video y compruébalos.
  4. Añade un botón nuevo al DOM después de la carga y verifica que invoca sin necesidad de registrarlo.
  5. Comprueba en tu navegador si 'command' in HTMLButtonElement.prototype es cierto y prueba el respaldo desactivándolo.