Publicado em 7 de março de 2025
Os botões são essenciais para criar aplicativos da Web dinâmicos. Botões abrem menus, alternam ações e enviam formulários. Eles fornecem a base da interatividade na Web. Tornar os botões simples e acessíveis pode levar a alguns desafios surpreendentes. Os desenvolvedores que criam microfront-ends ou sistemas de componentes podem encontrar soluções que se tornam mais complexas do que o necessário. Embora os frameworks ajudem, a Web pode fazer mais aqui.
O Chrome 135 apresenta novos recursos para fornecer comportamento declarativo com
os novos atributos command
e commandfor
, melhorando e substituindo os
atributos popovertargetaction
e popovertarget
. Esses novos atributos podem
ser adicionados a botões, permitindo que o navegador resolva alguns problemas principais relacionados
à simplicidade e à acessibilidade e ofereça funcionalidades comuns integradas.
Padrões tradicionais
Criar comportamentos de botão sem um framework pode apresentar alguns desafios
interessantes à medida que o código de produção evolui. Embora o HTML ofereça manipuladores onclick
para
botões, eles geralmente não são permitidos fora de demonstrações ou tutoriais devido às regras da
política de segurança de conteúdo (CSP). Embora esses eventos sejam enviados em elementos
de botão, os botões geralmente são colocados em uma página para controlar outros elementos
que exigem código para controlar dois elementos de uma só vez. Você também precisa garantir que essa
interação seja acessível para usuários de tecnologia adaptativa. Isso geralmente leva a
um código parecido com este:
<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>
Essa abordagem pode ser um pouco frágil, e os frameworks têm como objetivo melhorar a ergonomia. Um padrão comum com um framework como o React pode envolver o mapeamento do clique para uma mudança 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>
</>
);
}
Muitos outros frameworks também têm como objetivo fornecer ergonomia semelhante. Por exemplo, isso pode ser escrito no AlpineJS como:
<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>
Ao escrever isso no Svelte, pode ficar mais ou menos assim:
<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>
Alguns sistemas ou bibliotecas de design podem ir além, fornecendo wrappers em torno de elementos de botão que encapsulam as mudanças de estado. Isso abstrai as mudanças de estado por trás de um componente de gatilho, trocando um pouco de flexibilidade por uma ergonomia melhorada:
import {MenuTrigger, MenuContent} from 'my-design-system';
function MyMenu({children}) {
return (
<MenuTrigger>
<button>Open Menu</button>
</MenuTrigger>
<MenuContent>{children}</MenuContent>
);
}
O comando e o padrão commandfor
Com os atributos command
e commandfor
, os botões agora podem realizar ações
em outros elementos de forma declarativa, trazendo a ergonomia de um framework sem
sacrificar a flexibilidade. O botão commandfor
usa um ID, semelhante ao
atributo for
, enquanto o command
aceita valores integrados, permitindo uma abordagem mais
portátil e intuitiva.
Exemplo: um botão de menu aberto com comando e commandfor
O HTML a seguir configura relações declarativas entre o botão e o
menu, o que permite que o navegador gerencie a lógica e a acessibilidade para você. Não é
preciso gerenciar aria-expanded
ou adicionar JavaScript extra.
<button commandfor="my-menu" command="show-popover">
Open Menu
</button>
<div popover id="my-menu">
<!-- ... -->
</div>
Comparação de command
e commandfor
com popovertargetaction
e popovertarget
Se você já usou popover
, talvez conheça os atributos popovertarget
e popovertargetaction
. Eles funcionam de maneira semelhante a commandfor
e
command
, respectivamente, exceto que são específicos para popovers. Os atributos command
e
commandfor
substituem completamente esses atributos mais antigos. Os novos
atributos oferecem suporte a tudo o que os atributos mais antigos ofereciam, além de adicionar novos
recursos.
Comandos integrados
O atributo command
tem um conjunto de comportamentos integrados que são mapeados para várias
APIs de elementos interativos:
show-popover
: é mapeado parael.showPopover()
.hide-popover
: é mapeado parael.hidePopover()
.toggle-popover
: é mapeado parael.togglePopover()
.show-modal
: é mapeado paradialogEl.showModal()
.close
: é mapeado paradialogEl.close()
.
Esses comandos são mapeados para as contrapartes do JavaScript, além de simplificar a
acessibilidade (como fornecer as relações equivalentes de aria-details
e aria-expanded
), o gerenciamento de foco e muito mais.
Exemplo: uma caixa de diálogo de confirmação com command
e 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>
Clicar no botão Delete Record abre a caixa de diálogo como um modal, enquanto
clicar nos botões Close, Cancel ou Delete fecha a caixa de diálogo e
envia um evento "close"
na caixa de diálogo, que tem uma propriedade returnValue
que corresponde ao valor do botão.
Isso reduz a necessidade de JavaScript além de um único listener de evento
na caixa de diálogo para determinar o que fazer a seguir:
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
Além dos comandos integrados, você pode definir comandos personalizados usando um
prefixo --
. Os comandos personalizados enviam um evento "command"
no elemento
de destino, assim como os comandos integrados, mas não executam nenhuma
lógica adicional, como os valores integrados. Isso oferece flexibilidade para
criar componentes que podem reagir a botões de várias maneiras sem precisar
fornecer componentes de wrapper, percorrer o DOM para o elemento de destino ou mapear
cliques de botão para mudanças de estado. Isso permite que você forneça uma API dentro do HTML para
os 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 no ShadowDOM
Como o atributo commandfor
usa um ID, há restrições em
cruzar o shadow DOM. Nesses casos, você pode usar a API JavaScript para definir
a propriedade .commandForElement
, que pode definir qualquer elemento, em raízes
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>
Propostas futuras podem oferecer uma maneira declarativa de compartilhar referências entre fronteiras de sombra, como a Proposta de destino de referência.
A seguir
Vamos continuar a explorar as possibilidades de novos comandos integrados para cobrir funcionalidades comuns usadas por sites. As ideias propostas são abordadas na Proposta de interface aberta. Algumas das ideias já exploradas:
- Abrir e fechar elementos
<details>
. - Um comando
"show-picker"
para elementos<input>
e<select>
, mapeando parashowPicker()
. - Comandos de reprodução para elementos
<video>
e<audio>
. - Copiar conteúdo de texto de elementos.
Agradecemos a contribuição da comunidade. Se você tiver sugestões, não deixe de enviar um problema no Open UI Issue Tracker.
Saiba mais
Saiba mais sobre command
e commandfor
na
especificação
e no
MDN.