Curso de fotografia de produto
12 aulas · certificado · comece hoje
Material 8 · Design para páginas e campanhas
Botão é a menor interface que existe e a que mais decide: é nele que o visitante vira lead, o lead vira cliente e o cliente salva, paga, cancela ou desiste. Este guia junta o que mudou até novembro de 2026 em botões de site, de software, de aplicativo, de e-mail, de formulário e de inteligência artificial, com mais de 60 exemplos que funcionam aqui mesmo na página, o código de cada um e cinco oficinas para treinar as decisões que mais dão errado.
Referências consultadas entre 11 e 12 de novembro de 2026, com pesquisa feita por cinco modelos do geo-orchestrator (Perplexity, Grok, GPT-6 Astra, Claude Fable 5.1 e Gemini 4 Argon) e conferida contra a documentação oficial. Os exemplos acompanham um caso fictício, a agência Mirante e seus clientes. Cada recurso novo da plataforma vem com um selo que diz se o seu navegador já tem o recurso e o que acontece quando não tem.
Landing page: a promessa em uma frase e um botão que diz o que acontece.
Tudo certo nas medidas. Envia o formulário: <button type="submit">. Enter num campo do formulário também envia.
<button class="btx" type="submit">
<svg aria-hidden="true" focusable="false" width="20" height="20"><use href="#i-bt-send"></use></svg>
<span class="btx-rotulo">Enviar inscrição</span>
</button>.btx { min-height: 44px; }Medidas calculadas no navegador com getBoundingClientRect e getComputedStyle; contraste pela fórmula de luminância relativa da WCAG 2.2. Sem JavaScript, o botão de exemplo continua visível com as medidas do estado inicial.
A pergunta que decide o elemento vem antes de qualquer escolha visual: o que acontece quando a pessoa clica? Se ela vai para outro endereço, é link (<a href>). Se algo acontece na mesma tela (salvar, enviar, abrir um menu, tocar um vídeo), é botão (<button>). A aparência não muda isso. Um link pode ter cara de botão, com fundo e borda, e um botão pode ter cara de link, sublinhado e sem moldura. O que não pode é trocar o elemento por causa da aparência.
A diferença aparece no uso de todo dia. Link abre em nova aba com Ctrl+clique, aceita clique do meio, deixa copiar o endereço, entra no histórico e é seguido pelos robôs de busca. Botão responde a Enter e a Espaço, pode enviar formulário e anuncia estado (aberto, ligado, carregando). Quando alguém faz um <div onclick> no lugar de qualquer um dos dois, perde tudo isso de uma vez: o elemento some da ordem do Tab, o leitor de tela não diz que é clicável e o teclado não aciona nada.
<a href>. Tente Ctrl+clique: abre em nova aba.<button type="button">, mesmo sublinhado.download. Se precisa gerar antes, o botão gera e o link baixa.| Situação | Elemento e hierarquia |
|---|---|
| O clique leva a outra URL | <a href>, mesmo com cara de botão |
| Executa algo na tela atual | <button type="button"> |
| Envia o formulário | <button type="submit"> |
| Vai para uma seção da página | Link com fragmento (href="#fontes") e destino que existe |
| Entrega arquivo pronto | Link com download; se o arquivo é gerado na hora, botão para gerar |
| Abre menu, diálogo ou painel | Botão com o estado do padrão: aria-expanded, popovertarget ou commandfor |
| Parece botão, mas navega | Continua link: só muda a classe |
| Parece link, mas executa ação | Continua botão: só muda a classe |
| É a ação principal da região | Primário, um por região visível |
| É uma alternativa relevante | Secundário ou tonal |
| É auxiliar ou pouco usada | Terciário (texto), sem perder legibilidade |
| É destrutiva | Rótulo que diz a consequência; cor vermelha não basta |
| Aceitar e rejeitar cookies | Mesmo peso visual e mesmo acesso para as duas |
| Várias ações relacionadas | Grupo; botão dividido só quando há uma ação padrão clara |
| O canal mudou (app, e-mail) | Mantém a intenção e a prioridade; a semântica passa a ser a da plataforma |
Sair da conta merece nota: parece navegação, mas muda estado no servidor. O padrão seguro é um formulário com method="post" e um botão de envio, porque link com GET pode ser disparado por pré-carregamento (o navegador busca a página antes do clique) e deslogar a pessoa sem ela pedir.
Todo botão tem as mesmas sete partes, mesmo quando algumas estão invisíveis. Saber o nome de cada uma ajuda a pedir a coisa certa para o time ou para o agente de código: “aumente a área de toque sem mudar o desenho” é um pedido preciso; “deixa o botão maior” não é.
| Parte | Função | Cuidado |
|---|---|---|
| Contêiner | Superfície, forma e limite | min-block-size e padding dão tamanho; nenhum deles muda a semântica |
| Rótulo | Diz a ação ou o destino | Verbo + objeto (“Salvar endereço”); um aria-label substitui o texto, então não pode contradizê-lo |
| Ícone inicial | Reconhecimento rápido | Redundante com o texto: aria-hidden="true" no SVG |
| Ícone final | Direção, expansão, destino externo | A seta para baixo não substitui aria-expanded; o ícone de link externo não avisa o leitor de tela sozinho |
| Anel de foco | Onde o teclado está | :focus-visible com outline de 2 a 3 px e outline-offset; cuidado com overflow: hidden no pai, que corta o anel |
| Área de toque | Onde o clique pega | Pode passar do desenho com um pseudo-elemento; nunca pode invadir o vizinho |
| Estado | Disponível, ocupado, ligado | O visual, o atributo e o comportamento precisam dizer a mesma coisa |
A hierarquia diz para o olho onde clicar primeiro. Quando dois botões primários dividem a mesma região, nenhum dos dois ganha: a pessoa para, compara e às vezes desiste. A regra prática é um primário por região visível (o cabeçalho de um cartão, o rodapé de um diálogo, a dobra de uma landing page), com as alternativas um ou dois degraus abaixo.
“Começar grátis”, “Falar com vendas” e “Ver planos”, todos preenchidos com a mesma cor. Num teste de cinco segundos, as pessoas citam a cor da marca e não sabem qual é o próximo passo.
“Começar o teste grátis” preenchido, “Falar com vendas” em contorno e “Ver planos” como link de texto. O próximo passo fica óbvio e as duas saídas continuam lá.
Na prática da Mirante (caso fictício), a hierarquia também segue a ordem de leitura: em formulário e diálogo, o primário fica no fim da linha, onde o olho termina. Isso conversa com a convenção do macOS e do Material, que põem a ação afirmativa à direita; o Windows clássico punha à esquerda. A regra da casa precisa ser uma só e ficar escrita no sistema de design, para o time não decidir caso a caso.
Quem desenha costuma entregar dois estados, o normal e o hover. Quem usa encontra dez. Os estados podem se somar (selecionado e com foco, carregando e indisponível), e cada um precisa de três coisas alinhadas: o que se vê, o atributo que o leitor de tela anuncia e o que o código deixa ou impede de acontecer. A grade abaixo mostra os dez com o sistema de exemplo deste guia; passe o teclado por ela para ver o anel de foco de verdade.
<button>:hover:focus-visible:activearia-busy="true"role="status"data-estadoaria-describedbyaria-disabled="true"disabledaria-pressed="true"| Estado | Visual | Atributo ou seletor | Comportamento |
|---|---|---|---|
| Padrão | Contraste de 4,5:1 no texto | nenhum | Clicável, na ordem do Tab |
| Hover | Fundo 10 a 15% mais escuro (ou claro, no escuro) | :hover dentro de @media (hover: hover) | Nunca revela a única forma de chegar a uma ação |
| Foco visível | Anel de 2 a 3 px com 3:1 contra o fundo | :focus-visible | Não muda a função; não pode ficar escondido atrás de barra fixa (2.4.11) |
| Pressionado | Escala de 0,97 ou fundo mais denso | :active | Ação dispara ao soltar, para dar chance de desistir (2.5.2) |
| Carregando | Indicador girando + verbo no gerúndio, largura mantida | aria-busy="true" + aria-disabled="true" | Ignora o segundo clique; continua com foco |
| Sucesso | Ícone de confirmação, cor de sucesso por 2 a 3 s | Mensagem em role="status" | Volta ao padrão ou leva ao próximo passo |
| Erro | Contorno de alerta + ícone + texto | aria-describedby apontando o motivo | Volta a funcionar; não apaga o que a pessoa digitou |
| Indisponível | Esmaecido, legível | aria-disabled="true" | Continua focável e explica o motivo; o script impede a ação |
| Desabilitado | Esmaecido | disabled | Sai do Tab e o leitor de tela pode pular; use só quando o motivo é óbvio |
| Selecionado | Preenchido ou com marca | aria-pressed, aria-checked, aria-current ou aria-selected, conforme o padrão | Persiste até a pessoa mudar |
O estado que mais quebra é o desabilitado. Um botão “Continuar” cinza, sem explicação, deixa a pessoa caçando o campo que falta. O caminho melhor é manter o botão ativo e, no clique, mostrar o que falta preenchendo os erros nos campos. Quando o desabilitado for inevitável, use aria-disabled="true" com o motivo ligado por aria-describedby: o botão continua alcançável pelo Tab e o leitor de tela lê a explicação.
estados.css/* Um botão, dez estados. Cor por token; o atributo manda no visual. */
.botao {
--_bg: var(--marca); --_fg: var(--sobre-marca);
background: var(--_bg); color: var(--_fg);
transition: background-color .15s ease, transform .12s ease;
}
@media (hover: hover) {
.botao:hover { --_bg: color-mix(in oklab, var(--marca) 86%, black); }
}
.botao:active { transform: scale(.97); }
.botao:focus-visible { outline: 3px solid var(--marca); outline-offset: 3px; }
/* Indisponível continua focável; o script impede a ação e o motivo é lido. */
.botao[aria-disabled="true"],
.botao:disabled {
--_bg: color-mix(in oklab, var(--texto) 10%, var(--fundo));
--_fg: color-mix(in oklab, var(--texto) 62%, var(--fundo));
transform: none;
}
.botao[aria-busy="true"] { cursor: progress; }
.botao[data-estado="ok"] { --_bg: var(--sucesso); }
.botao[data-estado="erro"] { --_bg: var(--fundo); --_fg: var(--perigo);
border-color: var(--perigo); }
.botao[aria-pressed="true"] { --_bg: var(--marca); --_fg: var(--sobre-marca); }
@media (prefers-reduced-motion: reduce) {
.botao { transition: none; }
.botao:active { transform: none; }
}
/* Alto contraste do Windows: a borda do sistema devolve a forma. */
@media (forced-colors: active) {
.botao { border: 2px solid ButtonText; }
}Três números aparecem em toda conversa sobre tamanho de botão, e cada um vem de um lugar diferente. O critério 2.5.8 da WCAG 2.2 (nível AA, o que a maioria das leis cobra) pede alvo de pelo menos 24 por 24 pixels de CSS, ou espaço livre suficiente em volta. O critério 2.5.5 (nível AAA) sobe para 44 por 44. A Apple recomenda 44 por 44 pontos no iPhone, e o Android, 48 por 48 dp (pixel independente de densidade). Ponto, dp e pixel de CSS são unidades lógicas parecidas, sem conversão exata entre si; na prática, quem desenha para o polegar mira em 44 a 48.
| Referência | Mínimo | Unidade | Observação |
|---|---|---|---|
| WCAG 2.2, 2.5.8 (AA) | 24 × 24 | px de CSS | Ou espaço: um círculo de 24 px centrado no alvo não pode encostar em outro alvo. Exceções: link dentro de frase, controle do navegador, equivalente na mesma página |
| WCAG 2.2, 2.5.5 (AAA) | 44 × 44 | px de CSS | Meta para telas de toque e públicos com mais dificuldade motora |
| Apple Human Interface Guidelines | 44 × 44 | pt | Vale para iPhone e iPad; o SwiftUI amplia a área de botões pequenos quando há espaço |
| Android e Material 3 | 48 × 48 | dp | LocalMinimumInteractiveComponentSize garante 48 dp mesmo no botão de 32 dp |
| Material 3 Expressive (2025) | 32, 40, 56, 96 e 136 | dp | Alturas de XS a XL; a área de toque continua com piso de 48 |
Dá para ter um botão pequeno no desenho e grande no dedo. O truque é um pseudo-elemento invisível, centrado no botão, com no mínimo 44 px de lado: o clique cai nele e conta como clique no botão. O cuidado é o vizinho. Se duas áreas invisíveis se sobrepõem, o toque no meio vai para o elemento que estiver por cima, e a pessoa aciona o botão errado. A oficina do alvo de toque mede isso com os círculos de 24 px do critério.
area-de-toque.css/* Desenho de 32 px, área de toque de 44 px, sem empurrar o layout. */
.botao-compacto {
position: relative;
min-block-size: 32px;
}
.botao-compacto::after {
content: "";
position: absolute;
inset: 50% auto auto 50%;
inline-size: max(100%, 44px);
block-size: max(100%, 44px);
transform: translate(-50%, -50%);
}
/* Vizinhos: deixe pelo menos 12 px entre botões compactos para as áreas não se cruzarem. */No celular, o lugar também conta. A parte de baixo da tela, no centro, é a mais fácil de alcançar com o polegar de quem segura o aparelho com uma mão; o canto de cima, do lado oposto à mão, é o mais difícil. Por isso a ação principal de um aplicativo costuma morar numa barra inferior ou num botão flutuante, e a de uma landing page no celular, numa barra fixa embaixo. A seção de aplicativo mostra os dois.
O texto do botão responde, antes do clique, à pergunta “o que acontece se eu apertar?”. O formato que melhor responde é verbo no infinitivo mais o objeto: “Salvar endereço”, “Baixar o guia”, “Agendar avaliação”. Em português, a caixa é de frase (só a primeira letra maiúscula), sem o Title Case herdado do inglês. E o rótulo descreve o resultado da ação, nunca o gesto: “Clique aqui”, “Saiba mais” e “OK” obrigam a pessoa a ler o entorno para entender, e quem navega pela lista de botões do leitor de tela ouve só uma sequência de “Saiba mais”.
| Onde | Fraco | Forte | Por quê |
|---|---|---|---|
| Landing de curso | Saiba mais | Ver o conteúdo das 12 aulas | Diz o que a pessoa vai encontrar |
| Formulário de orçamento | Enviar | Receber o orçamento em 24 h | Promete o resultado e o prazo |
| Teste grátis | Cadastre-se | Começar o teste de 14 dias | Tira o peso do cadastro e diz a duração |
| Diálogo de exclusão | OK | Excluir 14 contatos | Diz o que se perde, com número |
| Checkout | Continuar | Pagar R$ 149,90 com Pix | Valor e meio de pagamento evitam surpresa |
| E-mail de confirmação | Clique aqui | Confirmar meu e-mail | Funciona até sem a imagem e sem o texto em volta |
| Painel de software | Processar | Importar 2.340 leads | Verbo do domínio e tamanho da tarefa |
| Botão de IA | Usar IA | Resumir a conversa | A tecnologia não é a ação; o resumo é |
Primeira pessoa (“Quero meu desconto”, “Começar meu teste”) funciona bem em landing page porque a pessoa lê o botão como a frase dela. Em software, onde o botão é comando e se repete o dia inteiro, o infinitivo curto (“Salvar”, “Publicar”) cansa menos. Quando o rótulo fica longo, quem corta é o adjetivo, nunca o verbo nem o objeto.
Uma regra de acessibilidade mora aqui também, a 2.5.3 da WCAG (rótulo no nome): o nome que o leitor de tela anuncia precisa conter o texto visível, de preferência no começo. Quem usa comando de voz fala o que vê (“clicar em Salvar”); se o botão diz “Salvar” e o aria-label diz “Gravar alterações”, o comando não encontra o botão. Na bancada, marque “Trocar o nome acessível por Clique aqui” para ver a conferência reprovar.
Ícone ao lado do texto acelera o reconhecimento; ícone sozinho economiza espaço e cobra um preço em compreensão. Só a lupa, o X de fechar, a casinha e alguns poucos símbolos são entendidos sem texto pela maioria das pessoas, como mostra o guia de ícones desta coleção. O resto precisa de rótulo visível ou, no mínimo, de um nome acessível e de uma dica que apareça no foco e no ponteiro.
aria-hidden="true": o texto já dá o nome.aria-label; o desenho sozinho basta para quem enxerga.interestfor quando o navegador tem e um script curto quando não tem. titleO title não aparece no toque nem no teclado e é lido de forma irregular. Dê nome com texto ou aria-label.Três regras fecham o assunto. O SVG de dentro do botão leva aria-hidden="true" e focusable="false", para não ser anunciado duas vezes nem roubar um Tab em navegadores antigos. A dica (tooltip) repete o nome, mas não guarda informação que só existe nela, porque não aparece para todo mundo. E o tamanho do ícone acompanha a letra: 16 px para botão de 32, 20 px para o de 44 e 24 px para os grandes, com o traço ajustado como ensina o guia de ícones.
dica-por-interesse.html<!-- Botão só com ícone: o nome vem do aria-label; a dica repete o nome. -->
<button type="button" class="botao-icone" aria-label="Duplicar campanha"
interestfor="dica-duplicar">
<svg aria-hidden="true" focusable="false" width="20" height="20"><use href="#i-bt-copy"></use></svg>
</button>
<div id="dica-duplicar" popover="hint" role="tooltip">Duplicar campanha</div>
<style>
.botao-icone { anchor-name: --duplicar; }
#dica-duplicar {
position-anchor: --duplicar;
position-area: block-start; /* acima do botão */
position-try-fallbacks: flip-block; /* sem espaço, vai para baixo */
margin: 8px 0;
}
</style>
<script>
// Reserva: sem interestfor, mostra a dica no foco e no ponteiro, com 350 ms de espera.
const b = document.querySelector(".botao-icone");
const dica = document.getElementById("dica-duplicar");
if (!("interestForElement" in HTMLButtonElement.prototype) && dica.showPopover) {
let t;
const mostrar = () => { clearTimeout(t); t = setTimeout(() => dica.showPopover(), 350); };
const esconder = () => { clearTimeout(t); t = setTimeout(() => dica.hidePopover(), 150); };
b.addEventListener("pointerenter", mostrar);
b.addEventListener("pointerleave", esconder);
b.addEventListener("focus", mostrar);
b.addEventListener("blur", esconder);
}
</script>Num site, o botão carrega a conversão. A landing page tem uma promessa por dobra e um botão que a cumpre; o resto da página é prova. As peças abaixo são as que a Mirante (caso fictício) mais monta para os clientes: chamada principal, WhatsApp, pagamento, login, cookies, cartão inteiro clicável, carregar mais, compartilhar e copiar. Todas estão ligadas: clique, use o Tab e observe a linha de status de cada uma, que é o que o leitor de tela anuncia.
Agenda cheia em 30 dias, sem depender de indicação
wa.me, com o número só em dígitos e a mensagem pronta no parâmetro text. Avise que abre fora.type="submit" do formulário de checkout e ganha guarda contra clique duplo.12 aulas · certificado · comece hoje
::after; o botão “Salvar” fica por cima e continua independente. Nada de link dentro de link.aria-pressed; o número é visual e o anúncio diz o que mudou.navigator.share() abre a folha do sistema; sem ela, copia o link. O “Copiado” só aparece depois que a cópia deu certo. Pacote de três sessões
Role esta caixa: a barra de baixo fica presa no fim da tela, como numa landing page no celular.
Avaliação inicial, plano de cuidados e retorno em 30 dias, com lembrete por WhatsApp.
Pagamento em Pix ou cartão. Cancelamento grátis até 24 horas antes.
As três sessões valem por 90 dias a partir da primeira.
Fim do conteúdo. O botão continua visível e não cobre o texto, porque a barra reserva o próprio espaço.
position: sticky no fim do contêiner, com env(safe-area-inset-bottom) para não colar na barra de gestos. Quando um campo ganha foco, a barra não pode cobrir o campo nem a mensagem de erro (critério 2.4.11).cartao-inteiro.css/* Cartão inteiro clicável sem link dentro de link: o título carrega o <a>,
e o ::after estica a área até as bordas do cartão. */
.cartao { position: relative; }
.cartao h3 a { color: inherit; text-decoration: none; }
.cartao h3 a::after {
content: "";
position: absolute;
inset: 0; /* cobre o cartão inteiro */
border-radius: inherit;
}
/* Botões secundários (salvar, curtir) ficam por cima do link esticado. */
.cartao .acao-extra { position: relative; z-index: 1; }
/* O foco aparece no cartão, não só no título. */
.cartao:has(a:focus-visible) { outline: 3px solid var(--marca); outline-offset: 3px; }
.cartao h3 a:focus-visible { outline: none; }barra-fixa.html<!-- viewport-fit=cover libera a área da barra de gestos; resizes-content faz a página
encolher quando o teclado virtual abre (Chrome no Android). -->
<meta name="viewport"
content="width=device-width, initial-scale=1, viewport-fit=cover, interactive-widget=resizes-content">
<div class="barra-acao">
<p class="barra-preco">R$ 390 <small>ou 3× de R$ 130</small></p>
<a class="botao" href="/agendar/">Agendar a primeira sessão</a>
</div>
<style>
.barra-acao {
position: sticky; bottom: 0;
display: flex; gap: 12px; align-items: center;
padding: 12px 16px max(12px, env(safe-area-inset-bottom));
background: var(--fundo); border-top: 1px solid var(--borda);
}
.barra-acao .botao { flex: 1; min-block-size: 48px; }
/* O conteúdo rola por trás sem esconder o elemento focado (WCAG 2.4.11). */
html { scroll-padding-block-end: 96px; }
</style>
<script>
// Safari: sem interactive-widget, acompanha o teclado pelo visualViewport.
const barra = document.querySelector(".barra-acao");
if (window.visualViewport && !CSS.supports("bottom: env(keyboard-inset-height)")) {
const ajustar = () => {
const tampa = innerHeight - (visualViewport.height + visualViewport.offsetTop);
barra.style.transform = `translateY(${-Math.max(0, tampa)}px)`;
};
visualViewport.addEventListener("resize", ajustar);
visualViewport.addEventListener("scroll", ajustar);
}
</script>Uma nota sobre link que pré-carrega. Desde 2023, o Chrome e o Edge aceitam regras de especulação (Speculation Rules), um bloco JSON que manda o navegador pré-renderizar a próxima página quando a pessoa para o ponteiro sobre o link por cerca de 200 milissegundos. Num botão-link de “Ver planos”, a página seguinte abre na hora. Só vale para navegação sem efeito colateral: nunca para sair da conta, registrar conversão ou qualquer coisa que mude dado.
Em software, o botão vira ferramenta. A pessoa passa o dia na mesma tela, aprende onde cada coisa fica e quer velocidade: atalhos de teclado, barras compactas, botões que guardam alternativas e estados que persistem. O risco é inventar comportamento. Cada peça abaixo segue um padrão do APG (o guia de práticas de ARIA do W3C), que diz quais teclas fazem o quê e o que o leitor de tela precisa ouvir.
aria-pressed. Atalhos anunciados por aria-keyshortcuts.anchor-name quando o navegador tem. aria-expanded diz se está aberto.role="switch" com aria-checked; o nome não muda com o estado.aria-pressed. Trocar o texto para “Ativar som” e manter o aria-pressed confunde.min e max. Digitar 12 tem de funcionar.| Campanha | Envios | Ações |
|---|---|---|
| Black Friday · aquecimento | 12.480 | |
| Reativação de inativos | 3.912 |
<kbd> fica escondido do leitor de tela e o atalho vai em aria-keyshortcuts. O atributo só anuncia; o atalho precisa existir no código.flex-direction: row-reverse deixa o Tab ao contrário do olho.barra-de-ferramentas.js// Barra de ferramentas (padrão Toolbar do APG): um Tab para entrar, setas para andar.
// HTML: <div role="toolbar" aria-label="Formatação"> com <button>s dentro.
export function ligarBarra(barra) {
const botoes = [...barra.querySelectorAll("button:not([disabled])")];
botoes.forEach((b, i) => (b.tabIndex = i === 0 ? 0 : -1));
barra.addEventListener("keydown", (e) => {
const atual = botoes.indexOf(document.activeElement);
if (atual < 0) return;
const destino = {
ArrowRight: (atual + 1) % botoes.length,
ArrowLeft: (atual - 1 + botoes.length) % botoes.length,
Home: 0,
End: botoes.length - 1,
}[e.key];
if (destino === undefined) return;
e.preventDefault();
botoes[atual].tabIndex = -1; // só um botão fica na ordem do Tab
botoes[destino].tabIndex = 0;
botoes[destino].focus();
});
// Alternâncias (negrito, itálico) guardam o estado em aria-pressed.
barra.addEventListener("click", (e) => {
const b = e.target.closest("button[aria-pressed]");
if (b) b.setAttribute("aria-pressed", String(b.getAttribute("aria-pressed") !== "true"));
});
}botao-dividido.html<div class="dividido" role="group" aria-label="Publicar">
<button type="button" class="botao" id="publicar-agora">Publicar agora</button>
<button type="button" class="botao seta" aria-label="Outras formas de publicar"
popovertarget="menu-publicar" aria-haspopup="menu" aria-expanded="false">
<svg aria-hidden="true" focusable="false" width="20" height="20"><use href="#i-bt-chevron-down"></use></svg>
</button>
</div>
<div id="menu-publicar" popover role="menu" aria-label="Outras formas de publicar">
<button type="button" role="menuitem">Agendar publicação</button>
<button type="button" role="menuitem">Publicar para teste</button>
<button type="button" role="menuitem">Salvar como rascunho</button>
</div>
<style>
.dividido .botao:first-child { border-start-end-radius: 0; border-end-end-radius: 0; }
.dividido .seta { border-start-start-radius: 0; border-end-start-radius: 0; }
/* Âncora: o menu nasce embaixo da seta e vira para cima se faltar espaço. */
@supports (anchor-name: --a) {
.dividido .seta { anchor-name: --seta-publicar; }
#menu-publicar {
position-anchor: --seta-publicar;
position-area: block-end span-inline-start;
position-try-fallbacks: flip-block, flip-inline;
margin: 6px 0; inset: auto;
}
}
</style>
<script>
// aria-expanded acompanha o popover; setas andam entre os itens (padrão Menu Button).
const seta = document.querySelector(".dividido .seta");
const menu = document.getElementById("menu-publicar");
menu.addEventListener("toggle", (e) => {
seta.setAttribute("aria-expanded", String(e.newState === "open"));
if (e.newState === "open") menu.querySelector("[role=menuitem]").focus();
});
menu.addEventListener("keydown", (e) => {
const itens = [...menu.querySelectorAll("[role=menuitem]")];
const i = itens.indexOf(document.activeElement);
if (e.key === "ArrowDown") { e.preventDefault(); itens[(i + 1) % itens.length].focus(); }
if (e.key === "ArrowUp") { e.preventDefault(); itens[(i - 1 + itens.length) % itens.length].focus(); }
});
</script>Em painel de software, a densidade pede botões de 32 a 36 px, menores do que no celular. Tudo bem com mouse, desde que o vão entre eles respeite o círculo de 24 px do critério 2.5.8 e que a mesma tela, aberta num tablet, ganhe uma versão de toque. Container queries (consultas de contêiner, que leem o tamanho da caixa e não da janela) resolvem isso sem duplicar componente: o botão fica com 44 px quando o painel encolhe para largura de celular.
No aplicativo, o botão divide espaço com o polegar, com a barra de gestos do sistema e com o teclado virtual. E cada sistema tem a própria linguagem: em 2025, a Apple apresentou o Liquid Glass (vidro líquido) no iOS 26, com botões translúcidos em forma de cápsula, e o Google lançou o Material 3 Expressive, com grupos de botões que se espremem ao toque, botão dividido e cinco alturas, de 32 a 136 dp. Num PWA (site instalado como aplicativo), a regra é parecer nativo sem copiar a marca de ninguém.
aria-expanded, foca a primeira ação e fecha com Esc. A barra inferior usa links com aria-current="page" e alvo de 48 px.| Papel | iOS 26 · SwiftUI | Android · Compose Material 3 | Windows · WinUI 3 | Web |
|---|---|---|---|---|
| Primário | .borderedProminent ou .glassProminent | Button | AccentButtonStyle | <button> preenchido |
| Secundário | .bordered ou .glass | FilledTonalButton, OutlinedButton | Button | Contorno ou tonal |
| Texto | .borderless | TextButton | HyperlinkButton | Botão com cara de link |
| Destrutivo | role: .destructive | Cores de erro do tema | Sem estilo próprio: cor do tema | Variante vermelha + rótulo explícito |
| Dividido | Menu com primaryAction | SplitButtonLayout (Expressive) | SplitButton | Dois botões + popover |
| Grupo | ControlGroup | ButtonGroup (Expressive) | CommandBar | role="group" ou role="toolbar" |
| Flutuante | Botão em .toolbar de vidro | FloatingActionButton, FloatingActionButtonMenu | Não é padrão | Botão fixo com aria-expanded |
| Retorno tátil | .sensoryFeedback(.success, trigger:) | performHapticFeedback(HapticFeedbackType.Confirm) | Não se aplica | navigator.vibrate() só no Android; nunca o único sinal |
| Alvo mínimo | 44 × 44 pt | 48 × 48 dp | 32 px no mouse; 40 no toque | 24 px (AA), 44 (AAA) |
Botoes.swiftimport SwiftUI
// iOS 26 · Liquid Glass. Os estilos .glass e .glassProminent são do SDK 26.
struct BotoesDaConsulta: View {
@State private var confirmou = 0
var body: some View {
VStack(spacing: 16) {
Button("Confirmar consulta") { confirmou += 1 }
.buttonStyle(.glassProminent) // ação principal
.buttonBorderShape(.capsule)
.controlSize(.extraLarge)
.sensoryFeedback(.success, trigger: confirmou)
Button("Remarcar") { }
.buttonStyle(.glass) // alternativa
.controlSize(.large)
Button("Cancelar consulta", role: .destructive) { }
.buttonStyle(.bordered)
// Ícone sozinho: rótulo para o VoiceOver e alvo de 44 pt.
Button { } label: {
Image(systemName: "map")
.frame(minWidth: 44, minHeight: 44)
.contentShape(.rect)
}
.accessibilityLabel("Abrir o mapa da clínica")
}
.toolbar {
ToolbarItem(placement: .cancellationAction) { Button("Fechar", role: .cancel) { } }
ToolbarItem(placement: .confirmationAction) { Button("Salvar", role: .confirm) { } }
}
}
}Botoes.kt// Jetpack Compose · Material 3 Expressive (APIs experimentais anotadas).
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun BotoesDaConsulta(onConfirmar: () -> Unit) {
val haptics = LocalHapticFeedback.current
val altura = ButtonDefaults.MediumContainerHeight // 56 dp
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
// A forma muda ao pressionar (shapes: repouso e pressionado).
Button(
onClick = { onConfirmar(); haptics.performHapticFeedback(HapticFeedbackType.Confirm) },
shapes = ButtonDefaults.shapesFor(altura),
contentPadding = ButtonDefaults.contentPaddingFor(altura),
modifier = Modifier.heightIn(min = altura)
) { Text("Confirmar consulta") }
FilledTonalButton(onClick = { }) { Text("Remarcar") }
TextButton(onClick = { }) { Text("Cancelar consulta") }
// Botão dividido: ação padrão + menu com as alternativas.
SplitButtonLayout(
leadingButton = {
SplitButtonDefaults.LeadingButton(onClick = { }) { Text("Enviar lembrete") }
},
trailingButton = {
SplitButtonDefaults.TrailingButton(checked = false, onCheckedChange = { }) {
Icon(Icons.Default.KeyboardArrowDown, contentDescription = "Outras formas de enviar")
}
}
)
}
// LocalMinimumInteractiveComponentSize garante 48 dp de toque mesmo no botão de 32 dp.
}Três cuidados de celular valem para app nativo e para site. O teclado virtual cobre a barra fixa de baixo: no Chrome do Android, interactive-widget=resizes-content na meta viewport faz a página encolher junto e a barra subir; no Safari, um ouvinte de visualViewport resolve. A barra de gestos do sistema come os últimos pixels: env(safe-area-inset-bottom), com viewport-fit=cover, devolve o espaço. E o retorno tátil (vibração curta ao confirmar) é um enfeite: o Safari não tem navigator.vibrate, e quem desliga a vibração no aparelho também precisa saber que deu certo.
E-mail é a plataforma mais antiga onde a agência ainda desenha botão, e a mais traiçoeira. O Outlook clássico para Windows monta a mensagem com o motor de texto do Word, que ignora padding e border-radius no link: o botão bonito do Apple Mail vira um texto azul de 16 px. O novo Outlook para Windows, que roda sobre o Chromium, já desenha o botão como a web, mas ignora o bloco condicional do Outlook antigo. A saída que funciona nos dois é o botão à prova de bala (bulletproof): uma tabela com o link estilizado para todos e um retângulo vetorial (VML) só para o Word.
O botão é texto num fundo de célula, então sobrevive sem imagem. Um botão que é imagem some com ela.
Olá, Marina! Sua avaliação está marcada para sexta, 14 de novembro, às 15h.
| Confirmar presença |
Precisa remarcar? Escolher outro horário.
botao-email.html<!-- No <head>: declarar os dois temas para os clientes que respeitam. -->
<meta name="color-scheme" content="light dark">
<meta name="supported-color-schemes" content="light dark">
<style>
@media (prefers-color-scheme: dark) {
.botao-email { background-color: #7fd0ea !important; color: #07202b !important; }
}
/* Outlook.com e o novo Outlook marcam o modo escuro com estes atributos. */
[data-ogsb] .botao-email { background-color: #7fd0ea !important; }
[data-ogsc] .botao-email { color: #07202b !important; }
@media only screen and (max-width: 600px) {
.botao-email { display: block !important; width: 100% !important; box-sizing: border-box; }
}
</style>
<!-- No corpo: tabela, VML só para o Outlook clássico e o link para todos os outros. -->
<table role="presentation" cellspacing="0" cellpadding="0" border="0" align="center">
<tr>
<td align="center" bgcolor="#0f5e7a" style="border-radius:10px;">
<!--[if mso]>
<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word"
href="https://exemplo.com.br/confirmar?utm_source=email" arcsize="21%" stroke="f"
fillcolor="#0f5e7a" style="height:48px;v-text-anchor:middle;width:240px;">
<w:anchorlock/>
<center style="color:#ffffff;font-family:Arial,sans-serif;font-size:16px;font-weight:bold;">
Confirmar presença
</center>
</v:roundrect>
<![endif]-->
<!--[if !mso]><!-->
<a class="botao-email" href="https://exemplo.com.br/confirmar?utm_source=email"
style="display:inline-block;padding:14px 28px;border-radius:10px;
background-color:#0f5e7a;background-image:linear-gradient(#0f5e7a,#0f5e7a);
color:#ffffff;font:bold 16px/20px Arial,sans-serif;text-decoration:none;">
Confirmar presença
</a>
<!--<![endif]-->
</td>
</tr>
</table>
<!-- E-mail não lê variável de CSS: aqui a cor vai escrita. O background-image repetindo
a cor impede que o Gmail do celular inverta o fundo do botão. -->| Cliente | Padding e raio no link | Lê o bloco do Outlook (VML) | Modo escuro |
|---|---|---|---|
| Outlook clássico (Windows) | Não | Sim | Inverte parte das cores |
| Novo Outlook (Windows) e Outlook.com | Sim | Não | Inversão parcial; marca com [data-ogsc] e [data-ogsb] |
| Gmail (web e aplicativo) | Sim | Não | O aplicativo inverte por conta própria; ignora prefers-color-scheme |
| Apple Mail (macOS e iOS) | Sim | Não | Respeita prefers-color-scheme |
Quatro regras dão conta da maior parte dos casos. O botão tem de 44 a 48 px de altura e texto de 16 px, porque boa parte das aberturas acontece no celular. O rótulo diz a ação (“Confirmar presença”), nunca o endereço. A cor do botão vai no fundo da célula e no link, para o modo escuro não apagar um dos dois. E o botão principal aparece uma vez só, no topo da mensagem, com um link de texto como saída alternativa.
Dentro de um <form>, todo <button> sem type é de envio. É o esquecimento mais comum da web: o botão “Adicionar outro telefone” envia o formulário pela metade. A regra é escrever o type sempre, mesmo quando parece óbvio. E vale saber o que o HTML já faz sozinho: Enter num campo de texto aciona o primeiro botão de envio do formulário, cada botão pode mandar para um destino diferente com formaction e o rascunho pode pular a validação com formnovalidate.
| Atributo | O que faz | Uso típico |
|---|---|---|
type="submit | button | reset" | Envia, não faz nada sozinho ou volta aos valores iniciais | Sempre escrito; reset quase nunca vale o risco |
form="id" | Liga o botão a um formulário que está em outro lugar da página | Botão de salvar na barra fixa, fora do <form> |
formaction, formmethod | Troca destino e método só para este botão | “Exportar” e “Salvar” no mesmo formulário |
formnovalidate | Envia sem a validação nativa | “Salvar rascunho” com campos incompletos |
name + value | Manda qual botão foi usado | “Aprovar” e “Recusar” no mesmo formulário |
formmethod="dialog" no <dialog> | Fecha o diálogo e guarda o value em returnValue | Confirmar ou cancelar sem script |
O clique duplo merece a oficina do envio seguro inteira, porque é ali que nasce o pedido duplicado, a cobrança em dobro e o lead repetido no CRM. O resumo: enquanto a resposta não volta, o botão fica com aria-busy="true" e aria-disabled="true", o script ignora o segundo envio e a largura do botão não muda. Desabilitar com disabled também impede o segundo clique, mas tira o foco do botão e o leitor de tela perde o lugar. No React 19, o gancho useFormStatus entrega o pending sem estado manual.
O botão de inteligência artificial virou peça comum em 2025 e 2026, quase sempre com o ícone de brilho (sparkles) que Google, Microsoft, Notion e Adobe adotaram. O ícone ajuda a reconhecer, mas não explica. O rótulo precisa dizer o que vai sair (“Resumir a conversa”, “Sugerir assunto do e-mail”), e o botão precisa de três companheiros: parar, enquanto o texto está sendo gerado; gerar outra versão, sem apagar a anterior; e copiar ou usar o resultado. Quando a ação gasta crédito, o custo aparece antes do clique.
aria-pressed.A oficina do botão de IA simula um resumo com o texto chegando aos poucos e deixa ligar e desligar cada boa prática: rótulo pelo resultado, botão de parar, aviso de conteúdo gerado, região viva que anuncia o fim e o saldo de créditos. Desligue uma por vez e veja o que a pessoa perde.
Nos últimos dois anos, o HTML e o CSS ganharam recursos que tiram dezenas de linhas de JavaScript de cada botão. Um botão abre diálogo e popover só com atributos (commandfor e command). O menu se posiciona sozinho em relação ao botão, inclusive virando para cima quando falta espaço (anchor positioning, posicionamento por âncora). A cor do texto pode ser escolhida pelo próprio navegador para dar contraste (contrast-color()). Cada demonstração abaixo tem um selo que diz se o seu navegador já tem o recurso; quando não tem, a reserva assume e o botão funciona igual.
Os atributos commandfor (o id do alvo) e command (o que fazer) viraram Baseline em 12 de dezembro de 2025, quando o Safari 26.2 se juntou ao Chrome 135 e ao Firefox 144. Os comandos nativos são show-modal, close e request-close para diálogo e toggle-popover, show-popover e hide-popover para popover. Comandos próprios começam com dois traços e disparam o evento command no alvo.
command="show-modal"; os botões do diálogo usam request-close e value. command="--somar" dispara o evento command no <output>, que decide o que fazer.comandos.html<!-- O botão diz o que fazer (command) e em quem (commandfor). Nenhuma linha de script. -->
<button type="button" commandfor="excluir" command="show-modal">Excluir lista de contatos</button>
<dialog id="excluir" closedby="any" aria-labelledby="excluir-titulo">
<h2 id="excluir-titulo">Excluir a lista “Leads de outubro”?</h2>
<p>São 2.340 contatos. Eles podem ser restaurados por 30 dias.</p>
<button type="button" commandfor="excluir" command="request-close" value="manter">Manter a lista</button>
<button type="button" commandfor="excluir" command="close" value="excluir">Excluir 2.340 contatos</button>
</dialog>
<!-- Comando próprio: começa com dois traços e dispara o evento "command" no alvo. -->
<button type="button" commandfor="contador" command="--somar">Somar 1</button>
<output id="contador">0</output>
<script>
document.getElementById("contador").addEventListener("command", (e) => {
if (e.command === "--somar") e.target.value = Number(e.target.value) + 1;
});
// Reserva para navegadores sem Invoker Commands (antes do Safari 26.2, Chrome 135 e Firefox 144).
if (!("commandForElement" in HTMLButtonElement.prototype)) {
document.addEventListener("click", (e) => {
const b = e.target.closest("button[commandfor]");
if (!b) return;
const alvo = document.getElementById(b.getAttribute("commandfor"));
const cmd = b.getAttribute("command");
if (cmd === "show-modal") alvo.showModal();
else if (cmd === "close" || cmd === "request-close") alvo.close(b.value);
else if (cmd.startsWith("--")) alvo.dispatchEvent(Object.assign(new Event("command"), { command: cmd, source: b }));
});
}
</script>O popover (popover + popovertarget) põe o menu na camada de cima da página, fecha com Esc e com clique fora e devolve o foco, sem uma linha de script; é Baseline desde janeiro de 2025. O posicionamento por âncora liga o menu ao botão: anchor-name no botão, position-anchor e position-area no menu e position-try-fallbacks: flip-block para virar quando bate na borda da tela. As propriedades centrais chegaram ao Safari 26 e ao Firefox 147 e viraram Baseline em janeiro de 2026. O botão dividido da vitrine de software usa os dois.
Um aviso: popover não é menu do ARIA. Para uma lista de comandos, o padrão continua pedindo role="menu", role="menuitem" e as setas do teclado. Para uma lista de links de navegação, basta o popover com links comuns, sem papel de menu.
O atributo interestfor mostra um popover quando a pessoa demonstra interesse no botão: ponteiro parado em cima, foco pelo teclado ou toque longo. Resolve a dica do botão só com ícone. Em novembro de 2026, só os navegadores Chromium têm (desde o 142); o Safari e o Firefox ainda não, por isso a dica do exemplo de ícone traz uma reserva de dez linhas. Junto vem o popover="hint", que abre uma dica sem fechar o menu que já estava aberto.
contrast-color(var(--marca)) devolve branco ou preto, o que der mais contraste com a cor de fundo; virou Baseline em abril de 2026. light-dark(claro, escuro) escolhe a cor pelo tema, sem duplicar regra. color-mix() deriva o hover da cor da marca misturando um pouco de preto, e a sintaxe relativa (oklch(from var(--marca) calc(l - .1) c h)) escurece só a luminosidade. Mesmo assim, a medida final é no botão desenhado: contrast-color() escolhe entre branco e preto, e às vezes nenhum dos dois chega a 4,5:1. A oficina de cor e estados mede isso.
@starting-style define de onde o menu começa ao aparecer, e transition-behavior: allow-discrete deixa animar a saída de um popover; os dois são Baseline desde agosto de 2024. corner-shape: squircle desenha o canto contínuo dos ícones do iPhone e só existe no Chromium 139 em diante; nos outros, o border-radius comum assume. text-box: trim-both cap alphabetic corta o espaço sobrando da fonte e centraliza o rótulo de verdade. E as consultas de estado de rolagem (container-type: scroll-state) deixam a barra fixa ganhar sombra só quando gruda no topo.
| Recurso | Para que serve no botão | Situação | No seu navegador |
|---|---|---|---|
:focus-visible, :has() | Anel de foco só no teclado; estilo do grupo pelo estado do filho | Amplamente disponível | |
| Popover | Menu e painel na camada de cima, Esc e clique fora | Baseline desde 27/01/2025 | |
| Invoker Commands | Abrir diálogo e popover por atributo | Baseline desde 12/12/2025 | |
requestClose() | Fechar com chance de cancelar (alterações não salvas) | Baseline desde 27/05/2025 | |
closedby | Diálogo que fecha com clique fora | Limitado: sem Safari estável | |
| Anchor positioning | Menu e dica presos ao botão | Propriedades centrais Baseline desde 13/01/2026 | |
interestfor | Dica no ponteiro e no foco | Limitado: só Chromium | |
contrast-color() | Texto branco ou preto automático | Baseline desde 10/04/2026 | |
light-dark() | Cor por tema numa linha | Baseline desde 13/05/2024 | |
| View Transitions na mesma página | Botão que vira outro estado com continuidade | Baseline desde 14/10/2025 | |
@starting-style | Entrada animada de menu e popover | Baseline desde 06/08/2024 | |
sibling-index() | Atraso em cascata num grupo de botões | Baseline desde 18/08/2026 | |
corner-shape | Canto squircle | Limitado: só Chromium | |
text-box | Rótulo centrado pela altura das letras | Em consolidação (a confirmar no agregado) | |
| Select personalizável | <button> dentro do <select> com appearance: base-select | Limitado: Chromium e Safari 27 | |
| Consultas de estado de rolagem | Barra fixa que muda ao grudar | Limitado: só Chromium | |
::scroll-button() | Botões de rolar gerados pelo CSS num carrossel | Limitado: só Chromium | |
if() no CSS | Valor condicional sem duplicar regra | Limitado: só Chromium |
A política desta coleção, a mesma do guia de Tailwind: recurso amplamente disponível entra direto; recurso que virou Baseline há pouco entra com @supports e reserva; recurso limitado só entra como melhoria que, se faltar, não tira nada de ninguém. O selo de cada recurso é confirmado no webstatus.dev no dia da entrega, nunca de memória.
A cor de marca costuma nascer para logotipo e anúncio. Por isso ela fica numa luminosidade média, chamativa demais para ser escura e escura demais para ser clara. O azul #3b82f6 com texto branco dá 3,68:1 e fica abaixo do mínimo para texto normal. Quando a Clínica Aurora pediu "o azul da marca no botão de agendar", a Mirante entregou o mesmo matiz um tom abaixo.
Um botão precisa passar em três medições diferentes. Quem cuida só do texto esquece as outras duas.
Token é uma variável com nome de função. A cadeia vai do primitivo (--azul-600, a tinta) ao semântico (--marca, o papel) e chega ao componente (--botao-fundo, o uso). Quando a Padaria Trigal trocou o laranja pelo vinho, a equipe mudou uma linha na camada semântica, e os botões vieram junto.
| Token | O que controla |
|---|---|
--botao-fundo | Preenchimento em repouso |
--botao-texto | Rótulo e ícone, com 4,5:1 contra o fundo |
--botao-borda | Contorno, com 3:1 contra a página nas variantes sem preenchimento |
--botao-hover | Fundo com o cursor em cima |
--botao-pressionado | Fundo durante o clique ou toque |
--botao-foco | Cor do anel de :focus-visible |
--botao-desabilitado | Fundo apagado de ação indisponível |
--botao-perigo | Ações destrutivas, como excluir |
OKLCH é um espaço de cor com luminosidade perceptual: baixar 0,1 em l escurece de forma parecida em qualquer matiz. A sintaxe relativa oklch(from var(--marca) calc(l - .1) c h) gera o pressionado, e um color-mix() da marca com um pouco de preto resolve o hover (o código abaixo traz a proporção). A função light-dark() devolve um valor para tema claro e outro para escuro, mas só funciona se color-scheme estiver declarado.
A função contrast-color() entrou no Baseline em 10/04/2026 (Chrome 147, Safari 26, Firefox 146) e escolhe branco ou preto para o texto. Ela escolhe o melhor dos dois e pode ainda ficar abaixo de 4,5:1 num fundo de luminosidade média. Meça cada combinação final.
No modo de alto contraste do Windows, o sistema troca suas cores pelas do usuário, e um botão sem borda some no meio da página. A consulta @media (forced-colors: active) devolve a borda com a cor de sistema ButtonText. Já prefers-contrast: more avisa que a pessoa pediu mais contraste, então reforce bordas e escureça o texto secundário.
tokens-botao.css@layer tokens {
:root {
color-scheme: light dark;
/* primitivos */
--azul-600: #2563eb;
--azul-300: #93c5fd;
--vermelho-700: #b91c1c;
--vermelho-300: #fca5a5;
--cinza-900: #111827;
--cinza-50: #f9fafb;
/* semânticos */
--marca: light-dark(var(--azul-600), var(--azul-300));
--perigo: light-dark(var(--vermelho-700), var(--vermelho-300));
--pagina: light-dark(var(--cinza-50), var(--cinza-900));
--tinta: light-dark(var(--cinza-900), var(--cinza-50));
/* componente */
--botao-fundo: var(--marca);
--botao-texto: contrast-color(var(--botao-fundo));
--botao-borda: var(--botao-fundo);
--botao-hover: color-mix(in oklab, var(--marca) 86%, black);
--botao-pressionado: oklch(from var(--marca) calc(l - .1) c h);
--botao-foco: var(--tinta);
--botao-desabilitado: color-mix(in oklab, var(--marca) 40%, var(--pagina));
--botao-perigo: var(--perigo);
}
@supports not (color: contrast-color(red)) {
:root { --botao-texto: light-dark(white, var(--cinza-900)); }
}
@media (prefers-contrast: more) {
:root { --botao-borda: var(--tinta); }
}
}
@media (forced-colors: active) {
.botao { border: 2px solid ButtonText; }
.botao:focus-visible { outline: 3px solid Highlight; }
}Para testar fundo, texto e estados lado a lado, abra a oficina de cor e estados.
O raio do canto passa personalidade antes de qualquer leitura. Com 2 px o botão parece sério, de 8 a 12 px fica neutro, e a pílula soa amigável. A Escola Ritmo usa pílula e um escritório de advocacia usaria 2 px. Defina um token por tamanho, porque o mesmo 12 px que fica bem num botão grande vira quase pílula num botão pequeno.
O squircle (canto contínuo, que curva sem emenda visível) chega via corner-shape: squircle, por enquanto só no Chromium 139+. Os outros navegadores ignoram a propriedade e ficam com o border-radius, então basta declarar os dois. O Material 3 Expressive (2025) muda a forma do botão ao pressionar, com uma forma em repouso e outra pressionada (ButtonShapes), e tem grupos em que os vizinhos se espremem. O Liquid Glass da Apple, no iOS 26, usa cápsulas translúcidas. Antes de copiar a translucidez, confira o contraste sobre o fundo mais claro e o mais escuro que podem passar por trás.
| Momento | Duração e propriedade | Por quê |
|---|---|---|
| Pressão | 80 a 120 ms, transform: scale(.97) | Confirma o toque sem atrasar a ação |
| Hover | 150 ms, opacity de uma camada | Suaviza a troca sem parecer lento |
| Entrada de menu | 160 a 200 ms, @starting-style | Mostra de onde o menu veio |
| Sucesso | 300 ms | Dá tempo de notar que deu certo |
| Limite | 500 ms | Acima disso, a pessoa fica esperando a interface |
Anime só transform e opacity, porque o navegador consegue animar essas duas sem recalcular o layout. Com prefers-reduced-motion, entregue direto o estado final. O indicador girando de "carregando" é uma exceção útil, e mesmo ele para nesse modo, trocado por um texto como "Enviando…".
Efeitos magnéticos, que puxam o botão para o cursor, e chuvas de partículas pesam no processador e tiram a atenção do conteúdo. Reserve esses efeitos para uma peça de campanha e nunca os coloque no botão de pagar. Retorno tátil, como sensoryFeedback no iOS, performHapticFeedback no Android e navigator.vibrate (que só funciona no Android), complementa o visual e nunca o substitui.
Na landing da Moda Alba, o botão "Comprar" pulsava em ciclos de 900 ms sem parar. A página ficava ansiosa e a vista era puxada para longe do produto.
O botão fica parado e responde com uma pressão de 100 ms. O destaque vem da cor e da posição na página.
movimento-botao.css.botao {
position: relative;
isolation: isolate;
border-radius: var(--raio-m);
transition: transform 100ms ease-out;
}
@supports (corner-shape: squircle) {
.botao { corner-shape: squircle; }
}
.botao::before {
content: "";
position: absolute;
inset: 0;
z-index: -1;
border-radius: inherit;
background: var(--botao-hover);
opacity: 0;
transition: opacity 150ms ease;
}
.botao:hover::before { opacity: 1; }
.botao:active { transform: scale(.97); }
.menu[popover] {
opacity: 0;
transform: translateY(-4px);
transition-property: opacity, transform, overlay, display;
transition-duration: 180ms;
transition-behavior: allow-discrete;
}
.menu[popover]:popover-open { opacity: 1; transform: none; }
@starting-style {
.menu[popover]:popover-open { opacity: 0; transform: translateY(-4px); }
}
@media (prefers-reduced-motion: reduce) {
.botao, .botao::before, .menu[popover] { transition: none; }
.botao:active { transform: none; }
.botao .girando { display: none; }
.botao .texto-carregando { display: inline; }
}Um componente de botão reúne decisões que precisam continuar funcionando em cada tela. Na Mirante, esse contrato define variante, tamanho, forma, estado, ícone e elemento HTML antes de virar uma peça reutilizável.
<button> executa ações; <a href> navega para um destino.Atributos data-*, dados personalizados do HTML, deixam escolhas como data-v="secundario" visíveis na marcação. Classes como botao--secundario também funcionam; escolha uma convenção para evitar duas fontes de configuração.
Os exemplos usam ARIA, atributos que comunicam estados às tecnologias assistivas, para sinalizar indisponibilidade e processamento. aria-disabled apenas comunica o estado: o código de interação precisa impedir a ação, enquanto o atributo nativo disabled já altera o comportamento do botão.
botao.css@layer components {
:root {
--marca: #174fc4;
--marca-forte: #103b96;
--sobre-marca: #ffffff;
--superficie: #ffffff;
--foco: #172554;
--raio-botao: .5rem;
}
.botao {
display: inline-flex;
align-items: center;
justify-content: center;
gap: .5rem;
min-block-size: 2.75rem;
padding: .625rem 1rem;
border: 2px solid transparent;
border-radius: var(--raio-botao);
background: var(--marca);
color: var(--sobre-marca);
font: inherit;
font-weight: 600;
text-decoration: none;
cursor: pointer;
}
.botao[data-v="secundario"] {
background: var(--superficie);
border-color: var(--marca);
color: var(--marca);
}
.botao[data-t="compacto"] {
min-block-size: 2.5rem;
padding: .375rem .75rem;
}
.botao:focus-visible {
outline: 3px solid var(--foco);
outline-offset: 3px;
}
.botao:not(:disabled):not([aria-disabled="true"]):hover {
background: var(--marca-forte);
}
.botao[data-v="secundario"]:not(:disabled):not([aria-disabled="true"]):hover {
background: color-mix(in oklab, var(--marca) 10%, var(--superficie));
}
.botao:disabled,
.botao[aria-disabled="true"] {
cursor: not-allowed;
border-style: dashed;
}
.botao[aria-busy="true"] {
cursor: wait;
}
.botao svg {
inline-size: 1.25em;
block-size: 1.25em;
flex: none;
}
}A armadilha aparece quando um componente Button sempre produz <button> e recebe um redirecionamento no clique. Um link preserva recursos do navegador, como abrir em outra aba, copiar o endereço e apresentar o destino.
“Ver coleção” da Moda Alba usa um botão com redirecionamento por JavaScript. O visitante perde comportamentos próprios de um link.
“Ver coleção” renderiza um link com a mesma aparência dos botões. “Adicionar à sacola” continua sendo um botão de ação.
O polimorfismo, capacidade de escolher o elemento renderizado, pode usar uma propriedade as implementada pelo projeto. No Radix, há composição com asChild; no Base UI, a personalização usa render, respeitando o contrato de cada componente. Um componente Link separado, com os mesmos estilos, também mantém essa distinção clara.
No Tailwind CSS v4, @theme registra tokens que geram utilitários, como bg-marca a partir de --color-marca. A diretiva @utility define uma classe reutilizável para a base, enquanto variantes condicionais aplicam estilos conforme atributos e estados.
A classe data-[v=secundario]:bg-superficie responde ao valor de data-v. Já aria-disabled: e aria-busy: respondem aos respectivos atributos com valor true; o bloqueio da ação continua no comportamento do componente.
botao-tailwind.css@import "tailwindcss";
@theme {
--color-marca: #174fc4;
--color-sobre-marca: #ffffff;
--color-superficie: #ffffff;
--color-foco: #172554;
--radius-botao: .5rem;
}
@utility botao {
display: inline-flex;
align-items: center;
justify-content: center;
gap: .5rem;
min-height: 2.75rem;
padding: .625rem 1rem;
border: 2px solid transparent;
border-radius: var(--radius-botao);
background: var(--color-marca);
color: var(--color-sobre-marca);
font-weight: 600;
text-decoration: none;
&:focus-visible {
outline: 3px solid var(--color-foco);
outline-offset: 3px;
}
}
/* Uso no HTML */
<a href="/colecao"
data-v="secundario"
class="botao data-[v=secundario]:bg-superficie
data-[v=secundario]:text-marca
data-[v=secundario]:border-marca
aria-disabled:cursor-not-allowed
aria-busy:cursor-wait">
Ver coleção
</a>O TypeScript pode restringir combinações de propriedades com uma união discriminada, um tipo que separa alternativas válidas. Aqui, href escolhe o link; sem ele, o componente produz um botão com type="button" por padrão, evitando envios acidentais de formulário.
Botao.tsximport type { MouseEventHandler, ReactNode } from "react";
type Base = {
children: ReactNode;
variante?: "primario" | "secundario";
tamanho?: "padrao" | "compacto";
carregando?: boolean;
icone?: ReactNode;
};
type Props = Base & (
| {
href: string;
type?: never;
onClick?: never;
}
| {
href?: never;
type?: "button" | "submit" | "reset";
onClick?: MouseEventHandler<HTMLButtonElement>;
}
);
export function Botao(props: Props) {
const {
variante = "primario",
tamanho = "padrao",
carregando = false,
icone,
children,
} = props;
const atributos = {
className: "botao",
"data-v": variante,
"data-t": tamanho,
"aria-busy": carregando || undefined,
"aria-disabled": carregando || undefined,
};
const conteudo = (
<>
{icone && <span aria-hidden="true">{icone}</span>}
<span>{children}</span>
</>
);
if (props.href !== undefined) {
return (
<a
{...atributos}
href={props.href}
onClick={(event) => {
if (carregando) event.preventDefault();
}}
>
{conteudo}
</a>
);
}
return (
<button
{...atributos}
type={props.type ?? "button"}
onClick={(event) => {
if (carregando) {
event.preventDefault();
return;
}
props.onClick?.(event);
}}
>
{conteudo}
</button>
);
}O exemplo preserva o foco durante o carregamento e impede a ativação comum por clique ou teclado. A proteção contra envios repetidos também deve existir no fluxo do formulário e no servidor; ações essenciais precisam validar permissões e dados independentemente da interface.
No React 19, useFormStatus, importado de react-dom, fornece pending para um componente descendente do formulário correspondente. useActionState, importado de react, reúne estado, ação e indicação de processamento; conecte esse estado à propriedade carregando.
React Aria Components oferece Button com onPress e isPending para coordenar interação e processamento. O hook usePress, do React Aria, normaliza toque, ponteiro e teclado; essas APIs ajudam a manter o comportamento consistente. No shadcn/ui, class-variance-authority, geralmente chamada de cva, organiza combinações de classes para variantes e tamanhos.
| Responsabilidade | CSS puro | Tailwind | React |
|---|---|---|---|
| Estilo | Tokens e seletores. | Tema, utilitários e variantes. | Seleciona classes ou atributos. |
| Semântica | Depende do HTML. | Depende do HTML. | Escolhe link ou botão. |
| Estado | Representa atributos. | Aplica variantes condicionais. | Coordena dados e bloqueia ações. |
| Teclado | Exibe foco. | Estiliza o foco. | Preserva comportamento nativo ou usa biblioteca. |
| Teste | Contraste e apresentação. | Combinações de classes. | Nome, papel, foco e interação. |
Com Playwright, ferramenta de automação de navegador, localize ações por getByRole('button', { name: 'Enviar' }) e destinos por getByRole('link', { name: 'Ver coleção' }). Verifique foco com toBeFocused(), ativação por teclado e ausência de envio duplicado durante o processamento.
Integre axe, mecanismo de verificações automatizadas de acessibilidade, para detectar parte dos problemas de estrutura e atributos. Complete a checagem com teclado e leitor de tela, incluindo o anúncio do carregamento e o retorno após erro. Para aprofundar a implementação, consulte o guia de Tailwind e o guia de React.
Toda temporada traz um botão da moda, e os clientes da Mirante pedem o que viram no celular na semana anterior. Algumas novidades resolvem problemas reais, outras só mudam a superfície. Antes de levar qualquer uma delas para a Clínica Aurora ou para a Moda Alba, convém saber de onde ela veio e qual risco carrega.
A Apple apresentou o Liquid Glass na WWDC 2025, junto com o iOS 26. No SwiftUI, os botões ganharam os estilos .glass e .glassProminent: cápsulas translúcidas que deixam o fundo aparecer por trás. O efeito impressiona, mas o contraste passa a depender do que está atrás do botão. Sobre uma foto clara, o rótulo branco some.
Na web, o caminho é o backdrop-filter (filtro aplicado ao conteúdo que fica atrás do elemento). Comece com um fundo opaco de reserva e só aplique o vidro quando o navegador suportar. Quem ativou prefers-reduced-transparency (preferência do sistema por menos transparência) deve receber o fundo opaco.
botao-vidro.css.bt-vidro {
background: var(--superficie); /* reserva opaca */
color: var(--texto);
border: 1px solid var(--borda);
border-radius: 999px;
}
@supports (backdrop-filter: blur(1px)) {
.bt-vidro {
background: color-mix(in srgb, var(--superficie) 60%, transparent);
backdrop-filter: blur(12px) saturate(1.4);
}
}
@media (prefers-reduced-transparency: reduce) {
.bt-vidro {
background: var(--superficie);
backdrop-filter: none;
}
}Em maio de 2025, o Google lançou o Material 3 Expressive. Ele trouxe cinco alturas: XS com 32, S com 40, M com 56, L com 96 e XL com 136 dp (pixel independente de densidade). Cada botão pode ter a forma round ou square. O pacote inclui ainda o botão dividido, o grupo de botões e um botão que muda de forma enquanto está pressionado.
O ícone de brilho virou o sinal de "aqui tem IA", e os sistemas de design criaram variantes só para isso. O Adobe Spectrum 2 tem genai e premium. O Atlassian tem discovery e rovo. A Escola Ritmo pode usar essa ideia num botão "Gerar plano de aula", desde que o rótulo diga o que acontece e não dependa só do brilho.
@propertyO @property registra uma propriedade customizada com tipo, e com isso o navegador consegue animar cores de gradiente. Sai barato quando o movimento anima só o background-position. Para não virar distração, o brilho corre uma ou duas vezes (ao carregar ou no hover) e para no estado final; laço infinito fica fora do portal. Com movimento reduzido ativado, o gradiente nem se mexe.
gradiente-vivo.css@property --gv-cor {
syntax: '<color>';
inherits: false;
initial-value: transparent;
}
.bt-gradiente {
--gv-cor: var(--marca);
color: var(--marca-contraste);
background: linear-gradient(110deg, var(--gv-cor), var(--marca-2), var(--gv-cor));
background-size: 200% 100%;
transition: --gv-cor 0.3s;
animation: gv-deslize 1.6s ease-in-out 2; /* duas passadas e para */
}
.bt-gradiente:hover {
--gv-cor: var(--marca-3);
animation: gv-deslize 1.6s ease-in-out 1; /* uma passada no hover */
}
@keyframes gv-deslize {
to { background-position: 200% 0; }
}
@media (prefers-reduced-motion: reduce) {
.bt-gradiente { animation: none; }
}Nos botões de login, a chave de acesso (passkey, credencial guardada no aparelho e liberada por biometria ou PIN) começa a ocupar o lugar da senha. No checkout da Moda Alba, "Pagar R$ 189,90 com Pix" informa mais que um "Finalizar" genérico. Em ações destrutivas, um aviso com "Desfazer" depois da ação substitui o diálogo de confirmação e poupa um clique de quem sabe o que está fazendo.
| Tendência | Quando usar | Cuidado |
|---|---|---|
| Vidro translúcido | Botões flutuantes sobre fotos ou mapas | Contraste variável; reserva opaca e prefers-reduced-transparency |
| Material 3 Expressive | Apps Android e telas com uma ação de destaque | Tamanhos L e XL ocupam muito espaço; reserve para a ação principal |
| Botão de IA | Ações que geram conteúdo | Rótulo com verbo claro; o brilho sozinho não explica a ação |
| Gradiente vivo | Uma chamada principal por tela | Animar só background-position e parar com movimento reduzido |
| Chave de acesso e Pix no rótulo | Login e checkout | Manter alternativa de senha e mostrar o valor exato |
| Desfazer no lugar de confirmar | Arquivar, remover, mover | Prazo visível e foco acessível no aviso |
Contraste, alvo de toque, rótulo e semântica continuam valendo, com ou sem moda. Um botão de vidro bonito que ninguém lê falha do mesmo jeito que um botão cinza mal escrito. A vitrine de site traz um botão de vidro e um gradiente vivo funcionando, e a seção de botões de IA mostra a variante com brilho aplicada.
As alturas abaixo são visuais. A área de toque pode ser maior que o desenho e merece outra conversa. Quando aparece "por composição", o sistema não oferece um componente dedicado: você monta a peça juntando outras. Valores em rem foram convertidos para px com fonte raiz de 16 px.
| Sistema | Variantes | Alturas | Raio | Dividido e grupo | Carregando |
|---|---|---|---|---|---|
| Material 3 (com Expressive) | elevated, filled, tonal, outlined, text; o Expressive acrescenta as formas round e square | M3 básico: S 40 dp. Expressive: XS 32, S 40, M 56, L 96, XL 136 dp | round: totalmente arredondado; square: 12, 12, 16, 28, 28 dp | No Expressive, SplitButton e ButtonGroup; o M3 básico não traz dividido como componente | Sem estado nativo; compor indicador |
| Apple HIG, Liquid Glass, iOS 26 | bordered, borderedProminent, borderless, plain, glass, glassProminent | A confirmar; dependem do estilo e do contexto. Área de toque recomendada: 44 × 44 pt | Cápsula nos botões com borda; varia conforme o contexto | Dividido a confirmar; agrupamento por layout | Em UIKit, UIButton.Configuration.showsActivityIndicator; desativação é separada |
| Microsoft Fluent 2 (React v9) | primary, secondary, outline, subtle, transparent | small 24, medium 32, large 40 px | 4 px no padrão; também circular e square | SplitButton; grupo por composição em barra de ferramentas | Ausente no Button v9; compor Spinner |
| IBM Carbon v11 | primary, secondary, tertiary, ghost, danger, danger--tertiary, danger--ghost | XS 24, S 32, M 40, L 48, XL 64 px; 2XL 80 px na documentação atual, disponibilidade em v11 a confirmar | 0 px no padrão; arredondamento da v12 é opcional na v11 | ComboButton (disponibilidade em v11 a confirmar); ButtonSet | Compor InlineLoading; suporte contextual em modal |
| Shopify Polaris (App Home) | auto, primary, secondary, tertiary; tone à parte | A confirmar; dependem da superfície | A confirmar | Dividido a confirmar; s-button-group | loading substitui o conteúdo e desativa o botão |
| Atlassian Design System | default, primary, subtle, warning, danger, discovery, rovo | default 32, compact 24 px | 6 px no padrão; 4 px no compacto | SplitButton e ButtonGroup | isLoading |
| GitHub Primer | default, primary, danger, invisible, link | small 28, medium 32, large 40 px | 6 px (--borderRadius-medium) | Dividido por composição com ButtonGroup e menu; ButtonGroup | loading, que preserva o foco |
| GOV.UK Design System | default, start, secondary, warning, inverse | Sem escala fixa; cerca de 38 px numa linha com fonte de 19 px | 0 px | Dividido não documentado; govuk-button-group | Ausente; data-prevent-double-click evita clique duplo, sem indicador |
| Adobe Spectrum 2 (React Spectrum S2) | primary, secondary, accent, negative, premium, genai; fillStyle: fill, outline | S, M, L, XL; valores em px a confirmar | Cápsula: metade da altura | Dividido a confirmar; ButtonGroup | isPending; spinner aparece após 1 segundo |
| Salesforce SLDS 2 (Lightning Web Components) | base, neutral, brand, brand-outline, destructive, destructive-text, inverse, success | Botão de texto a confirmar; lightning-button-icon: 16, 20, 24, 32 px | A confirmar | Dividido por composição de lightning-button e lightning-button-menu; lightning-button-group | Não documentado no lightning-button; is-loading no lightning-button-menu |
shadcn/ui (new-york-v4) | default, secondary, destructive, outline, ghost, link | xs 24, sm 32, default 36, lg 40 px | rounded-md; 8 px no tema de exemplo, configurável | Dividido por composição com ButtonGroup; ButtonGroup | Sem prop; compor Spinner e disabled |
| Radix Themes | classic, solid, soft, surface, outline, ghost | 1 24, 2 32, 3 40, 4 48 px | Configurável: none, small, medium, large, full | Ambos por composição, inclusive Toolbar | loading mostra spinner e desativa |
| Base UI | Sem variantes visuais: primitiva sem estilo | Definidas pela aplicação | Definido pela aplicação | Dividido por composição; Toolbar.Group | Sem prop; usar disabled e focusableWhenDisabled |
| React Aria Components | Sem variantes visuais: primitiva sem estilo | Definidas pela aplicação | Definido pela aplicação | Por composição; ButtonGroup a confirmar | isPending cuida do comportamento e do anúncio; o indicador visual é composto à parte |
primary no mesmo grupo, e raramente mais de um por página.Button para dar semântica de botão a um link; estilize o próprio <a> quando ele precisar parecer botão.Repare que três dessas regras tratam do texto. Para a Mirante, isso vira checklist de revisão: o rótulo de "Agendar consulta" da Clínica Aurora passa nos critérios de Material, Carbon e GOV.UK ao mesmo tempo. A regra do Primer resolve outra briga comum, a do cliente que quer três botões em destaque na mesma faixa.
No desktop, a altura padrão fica entre 32 e 44 px em quase todos os sistemas. Atlassian, Primer, Radix e Fluent apostam em 32 px; shadcn/ui usa 36; o GOV.UK chega perto de 38 px. Valores menores, como 24 px, aparecem em modos compactos e barras de ferramentas.
Base UI e React Aria seguem outra linha. Eles entregam comportamento, teclado e anúncio para leitores de tela, e deixam o visual inteiro por conta do projeto. Funcionam bem quando a agência já tem a identidade da Escola Ritmo pronta e só precisa de uma base acessível.
O estado de carregamento nativo existe em Atlassian, Primer, Polaris, Radix Themes, Spectrum 2 e React Aria. Nos demais, você compõe um indicador e cuida da desativação e do anúncio por conta própria. Na hora de escolher uma base para um projeto novo, essa diferença economiza trabalho.
Antes de repetir qualquer número sobre botão, veja o que ele mediu. Clique, início de teste grátis, pedido pago e consentimento de cookies são desfechos diferentes. Ganhar em um deles não garante ganho nos outros. Os aumentos citados abaixo são relativos, ou seja, calculados sobre a taxa original. A exceção são os resultados expressos em pontos percentuais, que medem a diferença absoluta entre duas taxas.
Um experimento do MobileHCI (2006) testou 20 pessoas usando com uma mão um aparelho daquele ano. Alvos de toque maiores, a área que responde ao dedo, geralmente deixaram o toque mais rápido. Os autores recomendaram 9,2 mm para toques isolados e 9,6 mm para sequências. O estudo trata de usabilidade e não mediu vendas. Celulares atuais também diferem bastante do aparelho testado.
Nos testes de checkout da Baymard Institute (2023), em desktop e celular, muitos participantes não encontravam a opção de comprar como visitante. Isso acontecia quando ela aparecia como link discreto ou abaixo da primeira área visível. A recomendação é dar a ela aparência de botão e colocá-la perto do topo. O achado trata de aparência e posição e não mostra que o elemento <button> converte mais que <a>.
Outra pesquisa da Baymard Institute (2012, atualizada em 2025) reuniu várias rodadas de testes. Nelas, participantes ignoravam botões “Aplicar” ou os confundiam com o botão principal de continuar, e alguns dados ficavam sem salvar. A saída sugerida é aplicar a maioria das entradas automaticamente, com possíveis exceções para cupom e cartão-presente. A pesquisa não define um número ideal de CTAs (chamadas para ação) por página.
O CXL publicou três testes de clique em que as pessoas recebiam uma tarefa e viam botões preenchidos ou botões fantasma, isto é, transparentes e só com contorno. Nos exemplos testados, os botões fantasma geraram mais erros e levaram cerca de 1 segundo a mais, em média, para serem clicados. A publicação não informa o número de participantes nem o ano. O resultado serve como indício sobre a facilidade de encontrar o botão.
Um experimento online de 2023 com 202 participantes usou uma loja simulada em três versões: com contador regressivo, com indicador de estoque ou sem nenhum dos dois. O contador piorou a avaliação prática da experiência. O indicador de estoque piorou a avaliação hedônica, que mede o quanto a visita foi agradável. Como as versões também mudavam descontos e etiquetas, e ninguém comprou de verdade, o efeito do texto do botão ficou sem medição isolada.
Num estudo apresentado no CHI 2020, 40 participantes residentes nos Estados Unidos navegaram com uma extensão que exibia banners de cookies diferentes. Retirar o “Rejeitar tudo” da primeira camada, que é o que aparece antes de qualquer clique, elevou a aceitação entre 22,0 e 23,1 pontos percentuais nesse estudo de 2020, conforme o tipo de aviso. A amostra era pequena.
Um experimento de 2024 com 358 participantes trouxe um resultado menos simples. Destacar “Rejeitar” no lugar de “Aceitar” elevou a rejeição de 27% para 34% nesse estudo de 2024, uma diferença sem significância estatística (p = 0,305, compatível com o acaso). Quando o destaque veio acompanhado de rótulos escritos como negações, a rejeição chegou a 80% no mesmo estudo de 2024. Nesse caso, porém, cor e texto mudaram juntos.
Os relatos abaixo vêm de testes A/B, em que duas versões são exibidas ao mesmo tempo para grupos diferentes. Eles foram divulgados pelas próprias empresas ou por blogs do setor. Quase nenhum publica a amostra completa, e vários mudaram mais de uma coisa por vez. Leia a coluna de ressalva antes da coluna de resultado.
| Teste | O que mudou | Resultado relatado | Ressalva |
|---|---|---|---|
| Going (Unbounce, 2024) | “Sign up for free” virou “Trial for free” | +104% na taxa de início do teste premium, de um mês para o outro | Os botões levavam a ofertas diferentes (plano grátis e teste premium); não há amostra nem comparação simultânea |
| Landing page da Unbounce (Michael Aagaard, 2013) | Rótulo em primeira pessoa (“my” no lugar de “your”) | +90% de cliques após três semanas | Amostra não publicada; os relatos divergem sobre o texto exato |
| Performable (Joshua Porter, 2011) | Botão vermelho contra botão verde | +21% de cliques no vermelho, com mais de 2.000 visitas em poucos dias | Mediu cliques numa página em 2011, sem acompanhar compras; não prova que o vermelho vence sempre, e o contraste com o resto da página é só hipótese |
| Botões fantasma (CXL) | Botões transparentes contra botões preenchidos | −20% de cliques em CTAs, em mais de 10.000 visitas | Significância declarada, mas nenhuma queda equivalente demonstrada em compras; ano não informado |
| CTA fixo no celular (GrowthRock, relato atualizado em 2026) | Barra fixa que abre gaveta para escolher o produto; outra barra fixa que só rolava até o botão | +5,2% de pedidos na versão com gaveta em 14 dias, significância declarada de 98%, mais de 3.000 pedidos por variante; a barra de rolagem não teve diferença significativa | O ganho pode vir da gaveta, além da fixação; ano do teste não informado |
| MusicLawContracts (CXL) | “download this contract now”, mais aviso de oferta limitada e contador | +147% de conversões | Texto, aviso e contador mudaram juntos; amostra, duração e ano não informados |
| Pluimen.nl (Unbounce, relato de 2024) | Página redesenhada com um único CTA e menos links | +19% de receita | Redesenho e corte de links aconteceram juntos; amostra e duração não informadas |
A Nielsen Norman Group (2019) recomenda rótulos específicos, que façam sentido mesmo lidos isoladamente. O texto cita uma participante que não sabia o que esperar de uma lista de “Learn more”. Isso apoia clareza e acessibilidade, mas a fonte não mede quanto um rótulo específico converte a mais que “Saiba mais” ou “Enviar”.
Em 2025, a NN/g separou ação de navegação: ações costumam ser botões, e destinos costumam ser links. Um destino pode ter cara de botão e continuar sendo <a href>, enquanto uma ação usa <button>. Trata-se de orientação de clareza, sem comparação de conversão.
Sobre botão desabilitado em formulário, a NN/g (2025) recomenda usá-lo com parcimônia e explicar por que ele está indisponível. O atributo disabled impede o acionamento e costuma tirar o botão da navegação por teclado. Já aria-disabled="true", sozinho, deixa a ação funcionando, então o bloqueio precisa ser programado. Manter o envio habilitado e mostrar erros claros é uma alternativa, mas a fonte não mede ganho com ela.
Para cookies, o guia orientativo da ANPD (2022) recomenda um botão visível para rejeitar cookies não necessários. O guia também considera negativa a prática de dificultar a rejeição enquanto só a aceitação ganha destaque. O relatório da força-tarefa de banners do EDPB (2023) não impõe uma cor ou um contraste único e pede avaliação caso a caso para identificar desenho enganoso. Nenhum dos dois documentos é um teste A/B, e uma taxa de aceitação maior não equivale a consentimento válido.
Botão vermelho vende mais que verde, rótulo em primeira pessoa sempre vence, urgência no texto multiplica conversões e página boa tem um botão só. Esses números circulam sem as ressalvas de origem: mediram cliques, mudaram várias coisas ao mesmo tempo ou não publicaram amostra.
Botão fantasma tende a ser encontrado com mais demora, e uma opção importante disfarçada de link discreto passa despercebida. Botões “Aplicar” extras confundem no checkout, e esconder a rejeição de cookies aumenta a aceitação. Cor, texto e quantidade de CTAs dependem do contexto.
Antes de mexer num botão como o “Agendar consulta” da Clínica Aurora, a Mirante testa com o público da própria clínica, com amostra e duração definidas antes de começar. O desfecho medido deve ser o que gera receita para o cliente, como pedido, agendamento ou matrícula, e não o clique. Cada rodada muda uma coisa só, para que o resultado tenha uma causa identificável.
Botão acessível é botão que funciona para mais gente: quem navega pelo teclado, quem usa leitor de tela, quem fala comandos de voz, quem tem tremor nas mãos, quem enxerga pouco contraste e quem está no sol com o celular. A WCAG 2.2 (diretrizes de acessibilidade do W3C, publicadas em outubro de 2023) é a referência internacional que as normas brasileiras de acessibilidade digital acompanham. As doze armadilhas abaixo respondem pela maior parte das reprovações em botão, cada uma com o critério que ela fere e a correção.
| Armadilha | Critério | Correção |
|---|---|---|
| Ícone sem nome, papel errado ou estado de alternância não exposto. | 4.1.2 — Nome, função, valor — A | Preferir controles nativos; fornecer nome acessível e atualizar aria-pressed, aria-checked ou aria-expanded conforme o padrão. |
| Ação disponível somente por mouse, toque ou gesto temporal. | 2.1.1 — Teclado — A | Permitir executar a mesma função por teclado, sem exigir temporização específica de teclas. |
| Remover o contorno e não oferecer outro indicador de foco. | 2.4.7 — Foco visível — AA | Definir estilo perceptível para :focus-visible; não usar outline: none sem substituição. |
| CTA fixo, banner ou toast esconder completamente o controle focado. | 2.4.11 — Foco não obscurecido (mínimo) — AA | Garantir que o elemento focado não fique totalmente oculto por conteúdo criado pelo autor. Preferir visibilidade integral. |
| Texto do botão com contraste insuficiente. | 1.4.3 — Contraste (mínimo) — AA | Exigir 4,5:1 para texto comum e 3:1 para texto grande: pelo menos 18 pt, ou 14 pt em negrito. Componentes inativos têm exceção. |
| Ícone, contorno ou indicador de estado necessário à identificação com baixo contraste. | 1.4.11 — Contraste não textual — AA | Usar pelo menos 3:1 contra cores adjacentes para informação visual necessária. Não exige borda em todo botão; componentes inativos têm exceção. |
| Alvos pequenos e próximos, especialmente ícones e ações de tabela. | 2.5.8 — Tamanho do alvo (mínimo) — AA | Buscar pelo menos 24 × 24 pixels CSS; o critério prevê alternativas e exceções, incluindo espaçamento, controles em linha e apresentação controlada pelo agente de usuário. |
| Arrastar como único modo de mover ou confirmar. | 2.5.7 — Movimentos de arrastar — AA | Oferecer alternativa de ponteiro único sem arrastar, como “Mover para cima/baixo”, salvo as exceções do critério. |
| Rótulo visível “Salvar” e nome acessível que não contém “Salvar”. | 2.5.3 — Rótulo no nome — A | Incluir o texto visível no nome acessível, preferencialmente no início: “Salvar alterações”. |
| Ação irreversível disparada no pressionamento inicial do ponteiro. | 2.5.2 — Cancelamento de ponteiro — A | Preferir ativação no evento de soltura, com possibilidade de abortar; observar as exceções e alternativas de cancelamento ou reversão previstas. |
| “Copiado”, “Salvo” ou novos resultados aparecem sem anúncio programático. | 4.1.3 — Mensagens de status — AA | Usar região como role="status" para mensagens pertinentes, sem exigir mudança de foco. Não anunciar cada atualização irrelevante. |
| Desfazer em toast desaparece antes de poder ser usado, sem alternativa. | 2.2.1 — Ajustável por temporização — A | Permitir desligar, ajustar ou estender o limite conforme o critério, ou fornecer recuperação sem prazo; observar suas exceções. |
forced-colors: active e prefers-reduced-motion: reduce: o botão continua com forma e o estado final aparece.O selo das ferramentas automáticas ajuda, mas não basta. O axe (motor de regras usado pelo Lighthouse e pelo DevTools) encontra nome ausente, contraste baixo e papel errado; ele não sabe se “Saiba mais” faz sentido, se o foco cai no lugar certo depois do envio nem se o toast some cedo demais. O teste acima cobre essa diferença em dois minutos por página.
As famílias se combinam: um botão pode ser primário, com ícone, de pagamento e fixo no celular ao mesmo tempo. Hierarquia, comportamento, plataforma e canal são dimensões diferentes, e o catálogo abaixo as separa para facilitar a consulta. A coluna do elemento é a mais importante: é ela que decide se o botão funciona no teclado, no leitor de tela e no clique do meio. Use a busca do navegador (Ctrl+F) para achar um tipo pelo nome.
| Tipo | Família | Quando usar | Elemento e ARIA | Armadilha comum | Rótulo de exemplo |
|---|---|---|---|---|---|
| Primário | Hierarquia · todos os canais | Destacar a ação principal do contexto. | <button type="button"> para ação; type="submit" para envio; <a href> para navegação. A aparência não determina o elemento. | Vários primários competindo na mesma região. | Criar minha conta |
| Secundário | Hierarquia · todos os canais | Oferecer alternativa relevante à ação principal. | <button> ou <a href>, conforme o resultado. Não existe papel ARIA de “secundário”. | Contraste insuficiente por ser menos importante. | Ver demonstração |
| Terciário / texto | Hierarquia · site, software e aplicativo | Apresentar ação de menor destaque, sem contêiner visual forte. | <button type="button"> para ação; <a href> para destino. | Parecer texto estático, sem pista de interação. | Agora não |
| Fantasma / ghost | Hierarquia · site e aplicativo | Usar preenchimento transparente, frequentemente com borda, sobre uma superfície. | <button> ou <a href>; sem ARIA especial. Defina a variante no design system. | Supor que “fantasma” significa necessariamente “secundário”; perder contraste sobre imagens. | Conhecer o projeto |
| Destrutivo | Hierarquia e intenção · software e aplicativo | Excluir, revogar ou executar ação de consequência negativa. | <button type="button">; type="submit" quando enviar o formulário de confirmação. | Comunicar risco somente com vermelho; usar confirmação genérica. | Excluir projeto |
| Somente ícone | Conteúdo · site, software e aplicativo | Representar comando reconhecível em espaço limitado. | <button type="button" aria-label="Fechar">; SVG decorativo com aria-hidden="true". | Confiar apenas no desenho ou no atributo title. | Fechar — nome acessível |
| Ícone com rótulo | Conteúdo · todos os canais | Reforçar reconhecimento sem depender de interpretação do ícone. | <button> com texto visível; ícone redundante com aria-hidden="true". | Leitor de tela anunciar ícone e texto duplicados. | Adicionar integrante |
| Alternância / toggle | Seleção e estado · software e aplicativo | Ligar e desligar um modo ou atributo da interface. | <button type="button" aria-pressed="false">; atualizar para "true". | Trocar o nome do controle a cada estado e tornar ambígua a relação com aria-pressed. | Silenciar |
| Switch | Seleção e estado · software e aplicativo | Alterar imediatamente uma configuração binária. | <button type="button" role="switch" aria-checked="false">; atualizar aria-checked, mantendo o nome. | Usar aria-pressed; oferecer estado intermediário, que não pertence ao padrão switch. | Receber notificações |
| Segmentado | Seleção e estado · site, software e aplicativo | Escolher uma opção entre poucas alternativas relacionadas. | Preferir <fieldset> com <legend> e <input type="radio"> estilizados. Se alternar painéis, usar o padrão tablist/tab/tabpanel. | Usar aria-selected em botões comuns ou misturar seleção única e múltipla. | Mensal / Anual |
| Grupo de botões | Composição · todos os canais | Reunir ações relacionadas sem transformá-las necessariamente em seleção. | Contêiner com role="group" e nome por aria-label ou aria-labelledby; botões nativos dentro. | Adotar role="toolbar" apenas por alinhamento visual. | Exportar CSV / Exportar PDF |
| Chip de filtro | Seleção e estado · site, software e aplicativo | Ativar filtros compactos, geralmente combináveis. | <button type="button" aria-pressed="false"> ou checkbox nativo estilizado. | Confundir selecionar filtro com remover uma etiqueta; aninhar botão de remoção. | Somente disponíveis |
| Curtir com contador | Seleção e estado · site e aplicativo | Registrar reação reversível e apresentar sua quantidade. | <button type="button" aria-pressed="false"> com nome estável; contador em texto associado ou descrito. | Usar o número como único nome ou anunciar atualizações de todas as reações continuamente. | Curtir · 128 |
| Seguir | Seleção e estado · site e aplicativo | Acompanhar perfil, assunto ou publicação. | Comando comum que alterna “Seguir”/“Deixar de seguir”; ou toggle com nome estável e aria-pressed. | Misturar os dois modelos sem comunicar claramente a consequência. | Seguir |
| Stepper de incremento/decremento | Entrada · site, software e aplicativo | Ajustar quantidade numérica em pequenos passos. | Dois <button type="button"> nomeados e campo <input type="number"> rotulado, com min, max e step pertinentes. | Expor somente “+” e “−” como nomes; permitir ultrapassar limites. | Aumentar quantidade |
| Split button | Composição · software e aplicativo | Combinar uma ação padrão com alternativas relacionadas. | Dois botões irmãos; somente o expansor recebe aria-haspopup="menu" e aria-expanded. | Um único alvo que executa e abre opções de forma imprevisível. | Publicar / Outras opções |
| Menu button | Composição · site, software e aplicativo | Abrir um menu de comandos. | <button type="button" aria-haspopup="menu" aria-expanded="false">; popup com role="menu" e itens apropriados, como role="menuitem". | Usar sem implementar foco, setas e Escape; chamar navegação comum de menu ARIA. | Mais ações |
| Barra de ferramentas | Composição · software e aplicativo | Concentrar comandos de edição ou manipulação frequentes. | Contêiner role="toolbar" nomeado; botões internos; navegação por setas e gerenciamento de tabindex. | Declarar toolbar sem seu modelo de teclado. | Alinhar à esquerda |
| Ribbon / faixa de opções | Composição · software | Organizar muitos comandos em categorias e grupos. | Não existe role="ribbon". Compor abas, painéis, grupos e toolbars conforme o comportamento real. | Inventar papel ARIA ou reproduzir a aparência sem estrutura e teclado consistentes. | Inserir tabela |
| Comando com atalho | Eficiência · software e aplicativo | Acelerar ações frequentes sem retirar o botão visível. | <button type="button" aria-keyshortcuts="Control+Enter">, quando esse atalho estiver realmente implementado. | Supor que aria-keyshortcuts implementa o atalho; colidir com navegador ou tecnologia assistiva. | Enviar — Ctrl+Enter |
| Ação por linha de tabela | Contexto · software | Editar, excluir ou abrir ações relativas a um registro. | <button> para comando ou <a href> para destino; nome contextual, como aria-label="Editar campanha de novembro". | Dezenas de controles chamados apenas “Editar”, sem contexto distinguível. | Editar |
| Carregando / loading | Execução · todos os canais interativos | Informar que uma operação está em andamento. | Botão pode receber aria-busy="true"; impedir duplicação com lógica e disabled ou aria-disabled, conforme necessário. Status em região separada. | Spinner sem nome; trocar todo o conteúdo e remover o nome acessível; liberar envios repetidos. | Salvando… |
| Com progresso | Execução · site, software e aplicativo | Mostrar avanço mensurável de uma operação. | Botão e <progress value="42" max="100"> nomeado, como elementos separados. Alternativa: role="progressbar" com valores ARIA. | Colocar outro widget dentro do botão; inventar porcentagem para operação indeterminada. | Enviando arquivo… |
| Segurar para confirmar | Confirmação · software e aplicativo | Acrescentar proteção contra ativação acidental em situações específicas. | <button type="button"> com duração controlada por script; instrução associada por aria-describedby e alternativa sem gesto temporal. | Exigir pressão prolongada de todos; disparar imediatamente em pointerdown. | Segure para encerrar |
| Deslizar para confirmar | Confirmação · aplicativo e web móvel | Acrescentar uma confirmação gestual quando houver justificativa de produto. | Componente customizado com alternativa por botão e teclado. Não há papel ARIA específico; role="slider" só serve se houver valor ajustável real. | Usar slider falso para um comando; exigir arrastar como único caminho. | Deslize para confirmar |
| Copiar | Utilidade · site, software e aplicativo | Copiar código, endereço, chave ou texto. | <button type="button">; navigator.clipboard.writeText() em contexto seguro; confirmação em role="status". | Informar sucesso antes da resolução da promessa; ignorar falhas e permissões. | Copiar código |
| Compartilhar — Web Share | Utilidade · site e aplicativo web | Abrir o compartilhamento oferecido pelo sistema. | <button type="button"> chama navigator.share() com ativação transitória do usuário; verificar disponibilidade e, para dados pertinentes, navigator.canShare(). | Presumir suporte universal; não oferecer alternativa; tratar cancelamento como falha grave. | Compartilhar |
| Download | Transferência · site, software e e-mail | Entregar um arquivo existente. | <a href="/arquivo.pdf" download> quando aplicável. Para arquivo remoto, cabeçalhos como Content-Disposition: attachment podem determinar o resultado. | Usar botão para um destino direto; supor que download funciona para qualquer URL entre origens. | Baixar relatório em PDF |
| Upload | Transferência · site, software e aplicativo | Selecionar arquivos para envio. | <input type="file"> com <label>; botão opcional para acionar o seletor, preservando acesso por teclado. | Esconder o único controle operável; tratar accept como validação de segurança. | Selecionar arquivo |
| Pagamento | Conversão · site e aplicativo | Confirmar uma compra ou iniciar a etapa de pagamento. | <button type="submit"> para confirmar formulário; <a href> para navegar ao checkout. | Cobrança inesperada; valor ausente; duplicação por cliques repetidos. | Pagar R$ 149,90 |
| Login social | Autenticação · site e aplicativo | Entrar por um provedor de identidade. | <a href> se navegar ao endpoint de autenticação; <button type="button"> se iniciar fluxo por SDK ou popup. | Escolher o elemento pela marca; pedir dados excessivos; ocultar abertura de janela. | Continuar com Google |
| Canal externo · site, e-mail e aplicativo | Abrir uma conversa ou atendimento no WhatsApp. | <a href="https://wa.me/5511999999999">; número internacional somente com dígitos. | Usar <button> para navegação; omitir que o destino é externo; inserir telefone formatado na URL. | Conversar no WhatsApp | |
| Voltar ao topo | Navegação · site e software | Levar à região inicial de página longa. | Preferir <a href="#topo"> com destino existente. Se executar rolagem programática, usar <button> e tratar orientação e foco quando necessário. | Mover só a tela e deixar o usuário de teclado perdido; animação obrigatória. | Voltar ao topo |
| Fechar | Contexto · site, software e aplicativo | Fechar diálogo, painel, aviso ou sobreposição. | <button type="button" aria-label="Fechar">; restaurar foco ao acionador quando apropriado. | Ícone “×” sem nome; fechar modal sem destino de foco válido. | Fechar |
| Link que parece botão | Semântica e aparência · todos os canais | Dar destaque visual a um destino. | <a href="/planos" class="button">; manter semântica de link, sem role="button". | Remover abrir em nova aba, copiar endereço e demais recursos nativos. | Conhecer os planos |
| Botão que parece link | Semântica e aparência · site, software e aplicativo | Executar comando discreto, sem aparência de botão preenchido. | <button type="button" class="link-like">. | Usar <a href="#"> para comando e provocar navegação ou salto indevido. | Limpar filtros |
| Paginação | Navegação · site e software | Navegar entre páginas de uma coleção. | <nav aria-label="Paginação"> com links reais; página atual com aria-current="page". Botões se forem comandos sem destino navegável. | Usar aria-selected em links comuns; desabilitar âncora com atributo disabled. | Próxima página |
| Carregar mais | Conteúdo incremental · site e aplicativo | Acrescentar resultados à coleção atual. | <button type="button">; status anuncia quantos itens foram adicionados; preservar foco. | Remover o botão focado sem tratamento; não informar novos resultados ou fim da lista. | Carregar mais resultados |
| Aceitar cookies | Consentimento · site e aplicativo web | Registrar aceitação das categorias opcionais indicadas. | <button type="button">; configurações de categoria usam controles próprios, como checkboxes. | Consentimento ambíguo; carregar rastreadores opcionais antes da decisão. | Aceitar todos |
| Rejeitar cookies | Consentimento · site e aplicativo web | Recusar categorias opcionais sem impedir o uso necessário. | <button type="button">; atualizar preferências efetivamente. | Esconder a recusa ou torná-la desproporcionalmente difícil; prometer rejeitar cookies estritamente necessários. | Rejeitar opcionais |
| Desfazer em toast | Recuperação · software e aplicativo | Reverter uma ação recente e reversível. | <button type="button"> junto à mensagem; anunciar o texto por role="status" sem mover foco automaticamente. | Prazo curto como único meio de recuperar a ação; toast desaparecer enquanto recebe foco. | Desfazer |
| Fixo no celular | Posicionamento · site e aplicativo web | Manter ação importante acessível em páginas longas. | <button> ou <a href>; CSS position: fixed ou sticky; considerar env(safe-area-inset-bottom). | Cobrir conteúdo, mensagens de erro ou controles focados; competir com teclado virtual. | Continuar compra |
| Cartão inteiro clicável | Área de interação · site e aplicativo | Ampliar o alvo de uma única navegação ou ação. | <a href> envolvendo conteúdo não interativo para destino; <button> apenas com conteúdo permitido, sem controles internos. | Aninhar links e botões; usar <div onclick>; bloquear seleção de texto. | Ver curso de fotografia |
| Alça de arrastar | Manipulação · software e aplicativo | Reordenar ou mover itens. | Pode ser <button type="button" aria-label="Mover etapa"> se oferecer operação por teclado; fornecer comandos equivalentes. | Tornar arrastar obrigatório; usar aria-grabbed ou aria-dropeffect, atributos obsoletos. | Mover etapa |
| Gerar com IA | IA · site, software e aplicativo | Criar conteúdo a partir de instruções e contexto. | <button type="button"> ou type="submit" em formulário; status de execução separado. | Não informar consumo de créditos, dados enviados ou necessidade de revisão. | Gerar descrição |
| Regenerar com IA | IA · software e aplicativo | Produzir nova versão a partir do contexto disponível. | <button type="button">; distinguir gerar alternativa de substituir conteúdo existente. | Apagar a versão anterior sem recuperação; ocultar novo custo. | Gerar outra versão |
| Parar IA | IA · software e aplicativo | Interromper geração ou transmissão em andamento. | <button type="button">; cancelar solicitação local quando possível e acionar cancelamento no serviço, se disponível. | Supor que interromper a conexão com AbortController necessariamente encerra processamento e cobrança no servidor. | Parar geração |
| FAB | Plataforma · Android e web com linguagem Material | Destacar uma ação principal frequente da tela. | Na web, <button> nomeado ou <a href> se navegar; no Android, componente FloatingActionButton pertinente à biblioteca. | Usar vários FABs concorrentes; encobrir conteúdo; escolher ação pouco importante. | Criar tarefa |
| FAB estendido | Plataforma · Android e web com linguagem Material | Explicitar a ação flutuante com ícone e texto. | Na web, botão ou link conforme função; em Compose Material 3, ExtendedFloatingActionButton. | Reduzir para ícone e perder o nome acessível ou a compreensão. | Nova mensagem |
| Speed dial | Composição · aplicativo e web móvel | Expandir um conjunto pequeno de ações relacionadas a um acionador flutuante. | Botão com aria-expanded e aria-controls; ações como botões/links. Usar aria-haspopup="menu" somente se implementar um menu real. | Ações disponíveis apenas em hover; foco escapar; sobrepor controles essenciais. | Criar |
| Botão nativo iOS | Plataforma · aplicativo iOS | Executar ações dentro de interface nativa. | SwiftUI Button ou UIKit UIButton; usar APIs nativas de acessibilidade. HTML e ARIA não se aplicam fora de conteúdo web. | Reconstruir o controle com gesto sem semântica; confundir pontos com pixels CSS. | Continuar |
| Botão nativo Android | Plataforma · aplicativo Android | Executar ações dentro de interface nativa. | Compose Material 3 Button ou, em Views, com.google.android.material.button.MaterialButton; semântica nativa, não ARIA. | Usar elemento visual clicável sem papel, nome e estado; confundir dp com pixel físico. | Salvar alterações |
| CTA de e-mail em tabela | Canal · e-mail HTML | Criar chamada para destino web com compatibilidade entre clientes. | <table role="presentation"> para layout, <td> para superfície e <a href> para navegação, com CSS inline apropriado. | Tornar somente o fundo da célula grande: padding no <td> não garante área inteira clicável. | Confirmar meu e-mail |
| CTA de e-mail com VML | Canal · e-mail HTML / Outlook clássico | Complementar o CTA para Outlook clássico no Windows com mecanismo de renderização Word. | <v:roundrect href="…"> em bloco condicional MSO e <a href> de fallback. É navegação; não adicionar role="button" ao link. | Supor suporte VML no novo Outlook ou no Outlook na web; URLs divergentes; duplicação visível por condicionais incorretas. | Acessar minha oferta |
| Enviar formulário | Formulário · site, software e aplicativo web | Submeter dados do formulário proprietário. | <button type="submit">; atributo form pode associar um formulário fora do ancestral imediato. | Esquecer que <button> dentro de formulário é submit por padrão quando type é omitido. | Enviar solicitação |
| Redefinir formulário | Formulário · site e software | Restaurar valores iniciais quando isso for realmente útil. | <button type="reset">; o comportamento nativo restaura valores iniciais, não necessariamente campos vazios. | Posicionar ao lado de enviar e causar perda acidental; rotular como “Limpar” sem corresponder ao resultado. | Restaurar valores iniciais |
| Expandir / recolher | Revelação · site, software e aplicativo | Exibir ou ocultar conteúdo associado. | <button type="button" aria-expanded="false" aria-controls="detalhes">; ou <details><summary>…</summary>…</details>. | Alterar apenas o ícone; ocultar visualmente conteúdo que continua exposto e focável. | Mostrar detalhes |
| Abrir diálogo | Revelação · site, software e aplicativo | Iniciar tarefa ou exibir conteúdo em diálogo. | <button type="button" aria-haspopup="dialog">; modal pode usar <dialog> aberto por showModal(), com nome acessível. | Abrir sobreposição sem foco inicial, contenção de foco modal e retorno ao acionador. | Editar endereço |
| Avançar / voltar em etapas | Fluxo · site e aplicativo | Percorrer um formulário ou processo em múltiplas etapas. | <button type="button"> para mudar etapa; type="submit" se submeter a etapa; <a href> se navegar a outra URL. | Fazer “Voltar” enviar formulário; mudar etapa sem orientar foco e título. | Próxima etapa |
| Buscar | Formulário e descoberta · site, software e aplicativo | Executar uma consulta explícita. | <button type="submit"> em formulário com campo de busca rotulado; formulário pode ter role="search". | Usar lupa sem nome; placeholder como único rótulo; consulta não preservar filtros relevantes. | Buscar cursos |
O mesmo “Ver relatório” feito de quatro jeitos parece igual na tela e se comporta de quatro jeitos diferentes no teclado, no leitor de tela e no clique do meio. Nesta oficina você passa o Tab, aperta Enter e Espaço em cada clone, roda a auditoria e depois usa o classificador para escolher o elemento certo de dez situações reais.
<div> com evento de clique, fora do Tab. Outro, feito com role="button", responde ao Enter e ignora o Espaço.<a href>; todo controle que age na tela é <button type="button">; nenhum href="#" nem div clicável.<a href="/relatorios/outubro/"> com a classe do botão.Sem JavaScript, o quadro aparece completo; a lista numerada descreve cada passo.
Use Tab, Enter, Espaço e o clique do meio do mouse. O registro ao lado conta o que cada um recebeu.
| Clone | Recebe Tab | Papel anunciado | Enter ativa | Espaço ativa | Abre em nova aba |
|---|
Escolha uma situação.
<!-- o código aparece aqui -->| Sintoma | Causa provável | Como confirmar | Correção |
|---|---|---|---|
| O Tab pula o botão | <div> com clique e sem tabindex | Passe o Tab e veja se o anel de foco aparece | Troque por <button type="button"> |
| Enter funciona, Espaço não | role="button" com ouvinte só de Enter | Foque e aperte Espaço: a página rola | Elemento nativo; o <button> já trata as duas teclas |
| “Abrir em nova aba” não existe | Botão com location.href no clique | Clique do meio e menu de contexto | <a href> com a classe do botão |
| A página pula para o topo | <a href="#"> usado como botão | O endereço ganha um # no fim | <button type="button"> |
| O formulário envia ao adicionar campo | <button> sem type dentro do form | A rede mostra um POST no clique | type="button" explícito |
| O leitor de tela diz “clicável” e nada mais | Evento em elemento genérico | Árvore de acessibilidade do DevTools: papel “generic” | Elemento nativo com nome visível |
link-ou-botao.html<!-- 1. Leva a outro endereço: link, mesmo com cara de botão. -->
<a class="botao" href="/relatorios/outubro/">Ver relatório</a>
<!-- 2. Faz algo na tela: botão com type explícito. -->
<button class="botao" type="button">Aplicar filtro</button>
<!-- 3. Envia o formulário. -->
<button class="botao" type="submit">Enviar inscrição</button>
<!-- 4. Abre um menu: o estado aberto ou fechado vai para o leitor de tela. -->
<button class="botao" type="button" aria-haspopup="menu" aria-expanded="false"
aria-controls="menu-campanha">Opções da campanha</button>
<!-- 5. Liga e desliga: o nome fica igual, o estado muda. -->
<button class="botao" type="button" aria-pressed="false">Silenciar notificações</button>
<!-- 6. Muda estado no servidor (sair, excluir): formulário POST. -->
<form method="post" action="/sair">
<button class="botao" type="submit">Sair da conta</button>
</form>
<!-- Nunca: --> <div class="botao" onclick="abrir()">Ver relatório</div>
<!-- Nunca: --> <a class="botao" href="#" onclick="abrir()">Ver relatório</a>Uma barra de ícones de 20 px com 2 px de vão parece normal num monitor e é um campo minado no polegar. O critério 2.5.8 da WCAG 2.2 mede isso com um círculo: centralize um círculo de 24 px em cada alvo menor que 24 px; se ele encosta noutro alvo ou noutro círculo, reprova. Mexa no lado e no vão dos cinco botões e ligue a área ampliada para ver quando o truque do pseudo-elemento resolve e quando ele cria um problema novo.
Sem JavaScript, o quadro aparece completo; a lista numerada descreve cada passo.
Os cinco alvos reprovam no critério 2.5.8.
| Sintoma | Causa provável | Como confirmar | Correção |
|---|---|---|---|
| Toca um, aciona o vizinho | Alvos menores que 24 px com vão curto | Círculos de 24 px se cruzam | Aumente o lado ou o vão |
| Área ampliada piorou | Pseudo-elementos de 44 px se sobrepõem | Contorno no ::after no DevTools | Vão de pelo menos 44 menos o lado |
| Passa no desktop, falha no celular | Tamanho em rem com fonte menor no celular | Meça a 390 px de largura | min-block-size: 44px fixo |
| Ação destrutiva cancelada sem querer | Cancelar colado em ação frequente | Ordem dos botões e distância | Isolar no fim, com desfazer |
| Link de texto reprova na auditoria | Ferramenta não aplicou a exceção de texto em linha | O link está dentro de uma frase? | Exceção válida; registre a justificativa |
medir-alvos.js// Teste do critério 2.5.8 (WCAG 2.2): alvo de 24 × 24 px ou espaço para o círculo de 24 px.
// Rode no console da página publicada, a 390 px de largura.
const alvos = [...document.querySelectorAll("a[href], button, [role=button], input, select, summary")]
.filter((el) => el.offsetParent !== null && !el.closest("p, li")) // texto em linha é exceção
.map((el) => ({ el, r: el.getBoundingClientRect() }));
const centro = (r) => [r.left + r.width / 2, r.top + r.height / 2];
const pequeno = ({ r }) => r.width < 24 || r.height < 24;
const cruza = (a, b) => a.left < b.right && b.left < a.right && a.top < b.bottom && b.top < a.bottom;
const reprovados = alvos.filter((a) => {
if (!pequeno(a)) return false;
const [cx, cy] = centro(a.r);
const circulo = { left: cx - 12, right: cx + 12, top: cy - 12, bottom: cy + 12 };
return alvos.some((b) => {
if (b === a) return false;
if (pequeno(b)) { // dois círculos: centros a menos de 24 px
const [bx, by] = centro(b.r);
return Math.hypot(cx - bx, cy - by) < 24;
}
return cruza(circulo, b.r); // círculo encosta num alvo grande
});
});
console.table(reprovados.map(({ el, r }) => ({
nome: el.getAttribute("aria-label") || el.textContent.trim().slice(0, 40),
largura: Math.round(r.width), altura: Math.round(r.height),
})));Rede lenta e dedo ansioso produzem o pedido duplicado. Esta oficina tem um formulário de inscrição com latência ajustável e quatro estratégias contra o segundo clique. Clique várias vezes seguidas no botão, troque a estratégia e leia o registro: ele conta as requisições, diz para onde foi o foco e avisa quando a largura do botão mudou.
min-inline-size; nada ao lado pula.aria-describedby.Sem JavaScript, o quadro aparece completo; a lista numerada descreve cada passo.
| Sintoma | Causa provável | Como confirmar | Correção |
|---|---|---|---|
| Inscrição em dobro no CRM | Nenhuma guarda contra o segundo envio | Duas requisições na aba Rede com 1 s de intervalo | Guarda no evento submit e chave de idempotência no servidor |
| O leitor de tela fica mudo | disabled tira o foco do botão | document.activeElement vira body | aria-disabled + guarda no script |
| O layout pula ao enviar | “Enviar” vira “Enviando…” e o botão cresce | Largura antes e depois no registro | min-inline-size do maior rótulo |
| Erro some sem explicação | Mensagem só em cor ou só no console | Simule a queda de rede | Texto do motivo + aria-describedby + role="status" |
| Formulário apagado depois do erro | Recarregar a página para “resetar” | Os campos voltam vazios | Manter os valores e só devolver o botão |
envio-seguro.js// Um envio por intenção: guarda no submit, aviso, foco preservado e volta com o motivo.
const form = document.querySelector("#inscricao");
const botao = form.querySelector("button[type=submit]");
const aviso = document.querySelector("#inscricao-status"); // <p role="status">
const erro = document.querySelector("#inscricao-erro");
let pendente = false;
let chave = crypto.randomUUID(); // uma chave por intenção, não por clique
form.addEventListener("submit", async (e) => {
e.preventDefault();
if (pendente) return; // segundo clique: nenhuma requisição nova
pendente = true;
const rotulo = botao.querySelector(".rotulo");
botao.setAttribute("aria-disabled", "true"); // continua focável (disabled tiraria o foco)
botao.setAttribute("aria-busy", "true");
rotulo.textContent = "Enviando…"; // min-inline-size no CSS segura a largura
aviso.textContent = "Enviando a inscrição.";
try {
const resposta = await fetch(form.action, {
method: "POST",
body: new FormData(form),
headers: { "Idempotency-Key": chave }, // o servidor recusa o duplicado
});
if (!resposta.ok) throw new Error(String(resposta.status));
rotulo.textContent = "Inscrição enviada";
chave = crypto.randomUUID(); // próxima inscrição, nova chave
aviso.textContent = "Inscrição enviada. A confirmação chega por e-mail.";
} catch {
rotulo.textContent = "Tentar de novo";
erro.hidden = false;
botao.setAttribute("aria-describedby", erro.id);
aviso.textContent = "Não foi possível enviar. Seus dados continuam no formulário.";
} finally {
pendente = false;
botao.removeAttribute("aria-disabled");
botao.removeAttribute("aria-busy");
}
});BotaoEnviar.tsx"use client";
import { useFormStatus } from "react-dom";
// React 19: o estado de envio vem do <form action={...}> pai, sem useState.
export function BotaoEnviar({ children = "Enviar inscrição" }: { children?: React.ReactNode }) {
const { pending } = useFormStatus();
return (
<button
type="submit"
aria-disabled={pending}
aria-busy={pending}
onClick={(e) => { if (pending) e.preventDefault(); }} // guarda contra o segundo clique
className="botao min-w-56"
>
{pending ? (
<>
<span className="giro" aria-hidden="true" />
Enviando…
</>
) : (
children
)}
</button>
);
}
// Uso:
// <form action={inscrever}>
// <input name="email" type="email" required />
// <BotaoEnviar />
// </form>A cor da marca quase nunca passa sozinha no contraste. Um azul médio com texto branco fica em torno de 3,7:1 e reprova; o mesmo azul no tema escuro, sobre fundo quase preto, some. Escolha uma cor (ou use uma das amostras), veja os estados padrão, hover, pressionado e desabilitado nos dois temas, com a medida de cada um, e copie os tokens com light-dark(), color-mix() e contrast-color(), já com a reserva para quem não tem.
color-mix, escurecendo no claro e clareando no escuro.light-dark().Sem JavaScript, o quadro aparece completo; a lista numerada descreve cada passo.
Calculando…
/* os tokens aparecem aqui */| Sintoma | Causa provável | Como confirmar | Correção |
|---|---|---|---|
| Texto branco “apagado” | Cor de marca de luminosidade média | Contraste abaixo de 4,5:1 | Texto escuro ou marca mais escura só no botão |
| Hover pior que o padrão | Hover clareia uma cor que já estava no limite | Meça o estado hover forçado no DevTools | No claro, o hover escurece |
| Botão some no tema escuro | A mesma cor nos dois temas | Botão contra a página abaixo de 3:1 | light-dark() com a versão clara |
| Anel de foco invisível | Anel na cor do botão sobre fundo parecido | Anel contra a página abaixo de 3:1 | Anel duplo (cor + halo do fundo) |
| Desabilitado ilegível | opacity: .4 no botão inteiro | Texto abaixo de 3:1 | Cores próprias de desabilitado, legíveis |
O bloco gerado acima é o código de referência: ele muda com a cor escolhida. A função de contraste é a mesma da WCAG 2.2, sobre a luminância relativa de cada cor em sRGB.
contraste.js// Contraste da WCAG 2.2 entre duas cores sRGB [r, g, b] de 0 a 255.
const canal = (v) => { v /= 255; return v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4; };
const luminancia = ([r, g, b]) => 0.2126 * canal(r) + 0.7152 * canal(g) + 0.0722 * canal(b);
export function contraste(a, b) {
const [claro, escuro] = [luminancia(a), luminancia(b)].sort((x, y) => y - x);
return (claro + 0.05) / (escuro + 0.05);
}
// Qualquer cor CSS (oklch, color-mix, hex) vira sRGB pelo canvas:
export function rgb(corCss) {
const c = new OffscreenCanvas(1, 1).getContext("2d");
c.fillStyle = corCss; c.fillRect(0, 0, 1, 1);
return [...c.getImageData(0, 0, 1, 1).data].slice(0, 3);
}
// contraste(rgb("white"), rgb("#3b82f6")) → 3,68 (reprova nos 4,5:1)Um botão de IA bem feito diz o que vai sair, deixa parar no meio, avisa que o texto foi gerado, anuncia o fim para quem não está olhando e mostra quanto custa. Esta oficina simula um resumo de conversa com o texto chegando aos poucos. Todas as boas práticas começam ligadas; desligue uma de cada vez e tente usar.
Sem JavaScript, o quadro aparece completo; a lista numerada descreve cada passo.
O resumo aparece aqui.
Texto gerado por IA a partir da conversa. Revise antes de enviar.
| Sintoma | Causa provável | Como confirmar | Correção |
|---|---|---|---|
| Ninguém sabe o que o botão faz | Rótulo pela tecnologia (“Usar IA”) | Pergunte a três pessoas o que vai acontecer | Verbo do resultado |
| A pessoa fica refém da geração | Sem parar | Tente interromper com o teclado | Parar no mesmo lugar e AbortController na requisição |
| Leitor de tela lê cada palavra | Região viva na caixa que recebe o texto em pedaços | Ative o leitor e gere | Anuncie início e fim numa região de status separada |
| Cliques compulsivos em “gerar de novo” | Custo invisível | Consumo de crédito por sessão | Custo e saldo ao lado do botão |
| Texto gerado enviado como se fosse da pessoa | Sem aviso nem revisão | Procure a marca de conteúdo gerado | Aviso visível e passo de revisão antes de usar |
botao-ia.js// Botão de IA: gerar, parar no mesmo lugar, anunciar início e fim, gerar outra versão.
const botao = document.querySelector("#resumir"); // <button>Resumir a conversa</button>
const saida = document.querySelector("#resumo"); // caixa sem aria-live
const aviso = document.querySelector("#resumo-status"); // <p role="status">
const versoes = [];
let controle = null;
botao.addEventListener("click", async () => {
if (controle) { controle.abort(); return; } // segundo clique = Parar
controle = new AbortController();
botao.querySelector(".rotulo").textContent = "Parar";
aviso.textContent = "Gerando o resumo."; // uma vez, não a cada palavra
saida.textContent = "";
try {
const r = await fetch("/api/resumo", { method: "POST", signal: controle.signal });
const leitor = r.body.pipeThrough(new TextDecoderStream()).getReader();
for (;;) {
const { value, done } = await leitor.read();
if (done) break;
saida.textContent += value;
}
versoes.push(saida.textContent); // a anterior não se perde
aviso.textContent = "Resumo pronto. Revise antes de enviar.";
} catch (e) {
aviso.textContent = e.name === "AbortError"
? "Geração interrompida. O texto parcial ficou na caixa."
: "Não foi possível resumir agora. Nenhum crédito foi usado.";
} finally {
controle = null;
botao.querySelector(".rotulo").textContent = "Resumir a conversa";
botao.focus();
}
});
// Atenção: abortar a conexão nem sempre interrompe o processamento e a cobrança no servidor;
// se a API tiver rota de cancelamento, chame-a também.Um agente de código (assistente de IA que edita arquivos do projeto) acerta mais quando o pedido chega com contrato. A coleção usa sempre o mesmo molde, e a Mirante cola esse molde no começo de cada tarefa. Peça um elemento por vez e a estrutura antes do visual: primeiro o HTML certo e o teclado, depois cor e movimento.
prompt-1.mdVocê é dev front-end sênior com foco em acessibilidade.
Tarefa: criar o componente Botao do site da Clínica Aurora.
Reutilize: tokens.css (cor, raio, espaço, foco) e base.css. Nenhum token novo sem avisar.
Variantes: primario, secundario, terciario, perigo.
Tamanhos: p (32 px), m (44 px), g (48 px).
Estados: repouso, hover, focus-visible, active, disabled, aria-disabled, carregando.
Aceite:
1. getBoundingClientRect() a 390 px de largura: p ≥ 24, m ≥ 44, g ≥ 48 de altura.
2. Contraste do texto ≥ 4,5:1 e do anel de foco ≥ 3:1, medidos no tema claro e no escuro.
3. @axe-core/playwright sem violação na página de vitrine.
4. Tab alcança todos; Enter e Espaço disparam; aria-disabled continua na ordem de foco.
5. Página de vitrine mostra as 4 variantes × 3 tamanhos × 7 estados.
Proibido: div clicável, href="#", outline: none sem substituto, hexadecimal solto, biblioteca nova sem informar o peso em kB.
Armadilha: hover recebe estilo e :focus-visible fica sem par; no tema escuro o anel some.
Entregue o diff e uma tabela com as medidas.prompt-2.mdVocê é revisor de interface com olho para semântica.
Tarefa: revisar landing.html da Moda Alba e corrigir só botões e links.
Reutilize: o componente Botao existente e os tokens.
Aceite:
1. Tudo que muda de endereço é <a href> com destino real; tudo que age é <button type>.
2. Cada rótulo começa por verbo e diz o objeto ("Ver coleção de verão").
3. Um único primário por região (topo, vitrine, rodapé).
4. Nome acessível contém o texto visível (WCAG 2.5.3), conferido no painel Acessibilidade.
5. axe sem violação; Tab percorre a página na ordem visual.
Proibido: div clicável, href="#", outline: none sem substituto, hexadecimal solto, biblioteca nova sem peso informado.
Armadilha: seis "Saiba mais" com o mesmo nome; o leitor de tela lista seis itens iguais.
Entregue uma tabela antes → depois de cada elemento alterado.prompt-3.mdVocê é dev front-end que cuida de formulários críticos.
Tarefa: botão "Enviar matrícula" da Escola Ritmo.
Reutilize: Botao, estado carregando e a região aria-live de avisos.
Aceite:
1. Teste Playwright com 10 cliques seguidos registra 1 requisição.
2. Durante o envio: aria-busy="true" no formulário e rótulo "Enviando matrícula…".
3. Erro mostra o motivo ("CPF já cadastrado") e leva o foco ao resumo de erros.
4. Sucesso é anunciado na região aria-live e o botão volta ao repouso.
5. Com rede lenta no DevTools, os três estados aparecem e são legíveis.
Proibido: div clicável, href="#", outline: none sem substituto, hexadecimal solto, biblioteca nova sem peso informado.
Armadilha: usar disabled durante o envio tira o foco do botão e o leitor de tela perde o contexto. Use aria-disabled e ignore o clique.prompt-4.mdVocê é dev front-end que conhece os recursos novos da plataforma.
Tarefa: botão dividido "Salvar | ▾" no painel da Mirante.
Use: atributo popover, commandfor/command e anchor positioning, com reserva.
Aceite:
1. A seta abre o menu via popover; Esc fecha e devolve o foco à seta.
2. No Chrome, o menu se ancora à seta com position-anchor.
3. Sem suporte a anchor positioning (@supports), o menu aparece logo abaixo, legível.
4. Setas do teclado percorrem os itens; Tab sai do menu.
5. A 390 px, getBoundingClientRect() do menu fica inteiro dentro da tela.
Proibido: div clicável, href="#", outline: none sem substituto, hexadecimal solto, biblioteca nova sem peso informado.
Armadilha: sem position-try-fallbacks o menu é cortado na borda direita do celular.prompt-5.mdVocê é dev de e-mail marketing.
Tarefa: botão "Agendar consulta" na newsletter da Clínica Aurora.
Reutilize: as cores do arquivo email-tokens.md e o modelo base.html.
Aceite:
1. Outlook para Windows recebe v:roundrect (VML) em comentário condicional; os demais, <a> com padding.
2. Área clicável inteira, altura ≥ 44 px, conferida no Litmus ou Email on Acid.
3. Rótulo em texto HTML; com imagens bloqueadas, o botão continua legível.
4. Modo escuro tratado com prefers-color-scheme e [data-ogsc]; contraste ≥ 4,5:1 nos dois modos.
5. Prévia aprovada em Outlook 365, Gmail no celular e Apple Mail.
Proibido: div clicável, href="#", outline: none sem substituto, hexadecimal solto fora do arquivo de tokens, biblioteca nova sem peso informado, botão feito de imagem.
Armadilha: só o texto fica clicável no Outlook quando o link envolve o rótulo e esquece a célula.Código gerado por IA passa por três portões. Primeiro, rodar: abrir a página, clicar, usar Tab e ver se funciona. Depois, medir cada item do aceite com a ferramenta indicada, sem confiar no resumo do agente. Por último, ler o diff (a lista de linhas alteradas) inteiro, procurando arquivos tocados sem pedido e qualquer item da linha "Proibido".
A lista leva uns vinte minutos por página. Cada item diz o que medir e onde.
link ou button coerente com a ação.getBoundingClientRect(); 44 px nos primários de celular.scroll-padding resolve).prefers-reduced-motion emulado, nada pula nem gira sem necessidade.forced-colors emulado, bordas e foco continuam visíveis.| Ferramenta | O que mede |
|---|---|
axe DevTools e @axe-core/playwright | Violações automáticas: nome ausente, contraste, papel incorreto |
| Lighthouse | Nota de acessibilidade e desempenho, peso de scripts |
| Impeccable | Padrões visuais fracos com npx impeccable detect --json, repetido com --viewport 390x844 |
| Painel Acessibilidade do DevTools | Papel, nome acessível e estados de cada elemento |
| Emulação no DevTools | Comportamento com prefers-reduced-motion e forced-colors |
| Playwright | Tab, Enter e Espaço em roteiro repetível, contagem de requisições |
| Litmus ou Email on Acid | Botões de e-mail em Outlook, Gmail e Apple Mail, claro e escuro |
O pull request leva o endereço de prévia e capturas a 1280 e 390 px nos dois temas. Junto vão as medidas de contraste e alvo, a saída do detector e a lista de aceite das oficinas marcada item por item. Quem revisa confere sem precisar rodar nada.
Ninguém precisa redesenhar o site para melhorar os botões. A Mirante aplica este roteiro em uma semana, começando pela página mais visitada do cliente.
document.querySelectorAll('a, button, [role=button], [onclick]') no console e conte quantos há de cada tipo.<button> e todo href="#" por um destino real ou por um botão.inventario.jsconst sel = 'a, button, [role=button], [onclick]';
const contagem = {};
document.querySelectorAll(sel).forEach((el) => {
let chave = el.tagName.toLowerCase();
if (el.getAttribute('href') === '#') chave += '[href=#]';
if (el.hasAttribute('onclick')) chave += '[onclick]';
contagem[chave] = (contagem[chave] || 0) + 1;
});
console.table(contagem);Depois do primeiro ciclo, siga as oficinas nesta ordem: link ou botão, alvo de toque, cor e estados, envio seguro e botão de IA. As duas primeiras corrigem a base, a terceira cuida do visual e as duas últimas tratam dos casos com mais risco. Cada uma termina numa lista de aceite que entra direto no PR.
Documentação oficial, critérios da WCAG 2.2, sistemas de design e estudos com autor e data, consultados em 11 e 12 de novembro de 2026. A pesquisa passou por cinco modelos do geo-orchestrator (Perplexity, GPT-6 Astra, Claude Fable 5.1, Gemini 4 Argon e Claude Opus 5.5) e cada fato de suporte de navegador foi conferido no Web Features Explorer. Os casos da agência Mirante e dos clientes citados são exemplos didáticos.
Plataforma web. MDN: Invoker Commands API; Web Features Explorer: invoker commands (Baseline em 12/12/2025); MDN: interest invokers; WHATWG: popover; Web Features Explorer: anchor positioning; MDN: dialog, closedby e requestClose; MDN: select personalizável; Web Features Explorer: contrast-color (Baseline em 10/04/2026); MDN: corner-shape; MDN: text-box-trim; Web Features Explorer: View Transitions; Web Features Explorer: Speculation Rules; W3C: relatório de implementação da Vibration API.
Acessibilidade. W3C: WCAG 2.2; W3C: Understanding 2.5.8 Target Size (Minimum); APG: Button Pattern; APG: Menu Button; APG: Toolbar; APG: Switch; W3C: WCAG 3.0, rascunho de 10/09/2026.
Aplicativos e sistemas operacionais. Apple: Human Interface Guidelines, Buttons; Apple: WWDC 2025, sessão 323 (Liquid Glass); Material 3: especificação de botões; Material 3: botão dividido; Material 3: grupos de botões; Microsoft Fluent 2: formas.
Sistemas de design. IBM Carbon; Shopify Polaris; Atlassian; GitHub Primer; GOV.UK Design System; Adobe Spectrum 2; Salesforce Lightning; shadcn/ui; Radix Themes; Base UI; React Aria Components.
Usabilidade e conversão. Parhi, Karlson e Bederson, MobileHCI 2006 (tamanho de alvo); Baymard: compra como visitante (2023); Baymard: botões Aplicar no checkout; CXL: botões fantasma; Nouwens e colegas, CHI 2020 (banners de cookies); estudo de 2024 sobre destaque e rótulos em banners de cookies; estudo de 2023 sobre escassez em loja simulada; Unbounce: CTA em primeira pessoa (relato de 2013); GrowthRock: botão fixo de compra; NN/g: rótulos de link melhores; NN/g: botões desabilitados; ANPD: guia orientativo de cookies (2022); EDPB: relatório da força-tarefa de banners de cookies (2023).
Guias desta coleção. Ícones e elementos visuais, Tailwind, tokens e CSS moderno, React, Navegação e Dashboards, cujas regras de contraste, foco, alvo de toque e política de adoção este guia reutiliza.