Fecha de publicación: 7 de marzo de 2025
Los botones son esenciales para crear aplicaciones web dinámicas. Los botones abren menús, activan o desactivan acciones y envían formularios. Proporcionan la base de la interactividad en la Web. Hacer que los botones sean simples y accesibles puede generar algunos desafíos sorprendentes. Los desarrolladores que compilan micro-frontends o sistemas de componentes pueden encontrar soluciones que se vuelven más complejas de lo necesario. Si bien los frameworks ayudan, la Web puede hacer más aquí.
Chrome 135 presenta nuevas funciones para proporcionar un comportamiento declarativo con los nuevos atributos command
y commandfor
, que mejoran y reemplazan los atributos popovertargetaction
y popovertarget
. Estos atributos nuevos se pueden agregar a los botones, lo que permite que el navegador aborde algunos problemas principales relacionados con la simplicidad y la accesibilidad, y proporcione una funcionalidad común integrada.
Patrones tradicionales
La compilación de comportamientos de botones sin un framework puede plantear algunos desafíos interesantes a medida que evoluciona el código de producción. Si bien HTML ofrece controladores onclick
a los botones, a menudo no se permiten fuera de las demostraciones o los instructivos debido a las reglas de la Política de Seguridad del Contenido (CSP). Si bien estos eventos se despachan en elementos de botón, los botones suelen colocarse en una página para controlar otros elementos que requieren código para controlar dos elementos a la vez. También debes asegurarte de que los usuarios de tecnología de accesibilidad puedan acceder a esta interacción. Esto suele provocar que el código se vea de la siguiente manera:
<div class="menu-wrapper">
<button class="menu-opener" aria-expanded="false">
Open Menu
</button>
<div popover class="menu-content">
<!-- ... -->
</div>
</div>
<script type="module">
document.addEventListener('click', e => {
const button = e.target;
if (button.matches('.menu-opener')) {
const menu = button
.closest('.menu-wrapper')
.querySelector('.menu-content');
if (menu) {
button.setAttribute('aria-expanded', 'true');
menu.showPopover();
menu.addEventListener('toggle', e => {
// reset back to aria-expanded=false on close
if (e.newState == 'closed') {
button.setAttribute('aria-expanded', 'false');
}
}, {once: true})
}
}
});
</script>
Este enfoque puede ser un poco inestable, y los frameworks tienen como objetivo mejorar la ergonomía. Un patrón común con un framework como React podría implicar asignar el clic a un cambio de estado:
function MyMenu({ children }) {
const [isOpen, setIsOpen] = useState(false);
const open = useCallback(() => setIsOpen(true), []);
const handleToggle = useCallback((e) => {
// popovers have light dismiss which influences our state
setIsOpen(e.newState === 'open')
}, []);
const popoverRef = useRef(null);
useEffect(() => {
if (popoverRef.current) {
if (isOpen) {
popoverRef.current.showPopover();
} else {
popoverRef.current.hidePopover();
}
}
}, [popoverRef, isOpen]);
return (
<>
<button onClick={open} aria-expanded={isOpen}>
Open Menu
</button>
<div popover onToggle={handleToggle} ref={popoverRef}>
{children}
</div>
</>
);
}
Muchos otros frameworks también tienen como objetivo proporcionar una ergonomía similar. Por ejemplo, esto se podría escribir en AlpineJS de la siguiente manera:
<div x-data="{open: false}">
<button @click="open = !open; $refs.popover.showPopover()" :aria-expanded="open">
Open Menu
</button>
<div popover x-ref="popover" @toggle="open = $event.newState === 'open'">
<!-- ... -->
</div>
</div>
Mientras escribes esto en Svelte, podría verse de la siguiente manera:
<script>
let popover;
let open = false;
function togglePopover() {
open ? popover.hidePopover() : popover.showPopover();
open = !open;
}
</script>
<button on:click={togglePopover} aria-expanded={open}>
Open Menu
</button>
<div bind:this={popover} popover>
<!-- ... -->
</div>
Algunos sistemas de diseño o bibliotecas pueden ir un paso más allá, ya que proporcionan wrappers alrededor de los elementos de botón que encapsulan los cambios de estado. Esto abstrae los cambios de estado detrás de un componente del activador, lo que cambia un poco la flexibilidad por una ergonomía mejorada:
import {MenuTrigger, MenuContent} from 'my-design-system';
function MyMenu({children}) {
return (
<MenuTrigger>
<button>Open Menu</button>
</MenuTrigger>
<MenuContent>{children}</MenuContent>
);
}
El comando y el patrón commandfor
Con los atributos command
y commandfor
, los botones ahora pueden realizar acciones en otros elementos de forma declarativa, lo que aporta la ergonomía de un framework sin sacrificar la flexibilidad. El botón commandfor
toma un ID, similar al atributo for
, mientras que command
acepta valores integrados, lo que permite un enfoque más intuitivo y portátil.
Ejemplo: Un botón de menú abierto con el comando command y commandfor
El siguiente código HTML establece relaciones declarativas entre el botón y el menú, lo que permite que el navegador controle la lógica y la accesibilidad por ti. No es necesario administrar aria-expanded
ni agregar ningún código JavaScript adicional.
<button commandfor="my-menu" command="show-popover">
Open Menu
</button>
<div popover id="my-menu">
<!-- ... -->
</div>
Comparación de command
y commandfor
con popovertargetaction
y popovertarget
Si ya usaste popover
, es posible que conozcas los atributos popovertarget
y popovertargetaction
. Funcionan de manera similar a commandfor
y command
, respectivamente, excepto que son específicos de los popovers. Los atributos command
y commandfor
reemplazan por completo estos atributos más antiguos. Los atributos nuevos admiten todo lo que hacían los atributos anteriores, además de agregar nuevas funciones.
Comandos integrados
El atributo command
tiene un conjunto de comportamientos integrados que se asignan a varias APIs para elementos interactivos:
show-popover
: Se asigna ael.showPopover()
.hide-popover
: Se asigna ael.hidePopover()
.toggle-popover
: Se asigna ael.togglePopover()
.show-modal
: Se asigna adialogEl.showModal()
.close
: Se asigna adialogEl.close()
.
Estos comandos se asignan a sus contrapartes de JavaScript y, al mismo tiempo, optimizan la accesibilidad (como proporcionar las relaciones equivalentes de aria-details
y aria-expanded
), la administración de enfoque y mucho más.
Ejemplo: Un diálogo de confirmación con command
y commandfor
<button commandfor="confirm-dialog" command="show-modal">
Delete Record
</button>
<dialog id="confirm-dialog">
<header>
<h1>Delete Record?</h1>
<button commandfor="confirm-dialog" command="close" aria-label="Close" value="close">
<img role="none" src="/close-icon.svg">
</button>
</header>
<p>Are you sure? This action cannot be undone</p>
<footer>
<button commandfor="confirm-dialog" command="close" value="cancel">
Cancel
</button>
<button commandfor="confirm-dialog" command="close" value="delete">
Delete
</button>
</footer>
</dialog>
Si haces clic en el botón Delete Record, se abrirá el diálogo como un diálogo modal. Si haces clic en los botones Close, Cancel o Delete, se cerrará el diálogo y, además, se enviará un evento "close"
en el diálogo, que tiene una propiedad returnValue
que coincide con el valor del botón.
Esto reduce la necesidad de JavaScript más allá de un solo objeto de escucha de eventos en el diálogo para determinar qué hacer a continuación:
dialog.addEventListener("close", (event) => {
if (event.target.returnValue == "cancel") {
console.log("cancel was clicked");
} else if (event.target.returnValue == "close") {
console.log("close was clicked");
} else if (event.target.returnValue == "delete") {
console.log("delete was clicked");
}
});
Comandos personalizados
Además de los comandos integrados, puedes definir comandos personalizados con un prefijo --
. Los comandos personalizados enviarán un evento "command"
al elemento objetivo (al igual que los comandos integrados), pero, de lo contrario, nunca realizarán ninguna lógica adicional como lo hacen los valores integrados. Esto brinda flexibilidad para compilar componentes que pueden reaccionar a los botones de varias maneras sin tener que proporcionar componentes de wrapper, recorrer el DOM para el elemento objetivo ni asignar clics de botones a cambios de estado. Esto te permite proporcionar una API dentro de HTML para tus componentes:
<button commandfor="the-image" command="--rotate-landscape">
Landscape
</button>
<button commandfor="the-image" command="--rotate-portrait">
Portrait
</button>
<img id="the-image" src="photo.jpg">
<script type="module">
const image = document.getElementById("the-image");
image.addEventListener("command", (event) => {
if ( event.command == "--rotate-landscape" ) {
image.style.rotate = "-90deg"
} else if ( event.command == "--rotate-portrait" ) {
image.style.rotate = "0deg"
}
});
</script>
Comandos en ShadowDOM
Dado que el atributo commandfor
toma un ID, existen restricciones en cuanto a cruzar el DOM sombreado. En estos casos, puedes usar la API de JavaScript para establecer la propiedad .commandForElement
, que puede establecer cualquier elemento en las raíces de sombra:
<my-element>
<template shadowrootmode=open>
<button command="show-popover">Show popover</button>
<slot></slot>
</template>
<div popover><!-- ... --></div>
</my-element>
<script>
customElements.define("my-element", class extends HTMLElement {
connectedCallback() {
const popover = this.querySelector('[popover]');
// The commandForElement can set cross-shadow root elements.
this.shadowRoot.querySelector('button').commandForElement = popover;
}
});
</script>
Las propuestas futuras pueden proporcionar una forma declarativa de compartir referencias entre los límites de la sombra, como la Propuesta de objetivo de referencia.
Próximos pasos
Seguiremos explorando las posibilidades de nuevos comandos integrados para cubrir las funciones comunes que usan los sitios web. Las ideas propuestas se incluyen en la Propuesta de IU abierta. Estas son algunas de las ideas que ya se exploraron:
- Abrir y cerrar elementos
<details>
- Un comando
"show-picker"
para elementos<input>
y<select>
que se asigna ashowPicker()
- Comandos de reproducción para elementos
<video>
y<audio>
. - Copiar contenido de texto de elementos
Agradecemos los aportes de la comunidad. Si tienes sugerencias, no dudes en enviar un problema al registro de problemas de la IU abierta.
Más información
Obtén más información sobre command
y commandfor
en la especificación y en MDN.