Material 8 · Design para páginas e campanhas

Botões: a promessa de que algo vai acontecer

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.

Bancada: monte um botão e veja as medidasMude o contexto, a intenção e o estado; as medidas são lidas do botão desenhado agora, no seu navegador.
Onde ele mora
O que acontece no clique
Hierarquia
Tamanho e forma
Ícone
Estado
Teste de estresse

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.

Alvo de 44 px de altura · mínimo de 24 (WCAG 2.5.8)Texto branco sobre azul-petróleo 7,2:1Nome acessível: “Enviar inscrição”

O HTML que esta combinação pedeatualiza ao vivo
<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>
O CSS por trás (tokens, estados e foco)por contexto
.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.

Antes da cor: um botão faz, um link leva

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.

Árvore de decisão. Quatro perguntas escolhem o elemento; a hierarquia visual vem depois.
Árvore de decisão entre link e botão Primeira pergunta: o clique muda o endereço? Se sim, link com href. Se não, segunda pergunta: envia um formulário? Se sim, botão type submit. Se não, terceira: abre ou fecha algo? Se sim, botão com aria-expanded, popovertarget ou commandfor. Se não, botão type button. O clique muda o endereço? simnão <a href> Envia um formulário? simnão type="submit" Abre ou fecha algo? simnão aria-expanded, popovertarget ou commandfor type="button"
Link com cara de botãoLeva a outro lugar, então é <a href>. Tente Ctrl+clique: abre em nova aba.

Botão com cara de linkFaz algo na tela, então é <button type="button">, mesmo sublinhado.
DownloadArquivo pronto é link com download. Se precisa gerar antes, o botão gera e o link baixa.
Quinze regras de decisão (taxonomia preparada com o GPT-6 Astra em 12/11/2026 e revisada)
SituaçãoElemento 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áginaLink com fragmento (href="#fontes") e destino que existe
Entrega arquivo prontoLink com download; se o arquivo é gerado na hora, botão para gerar
Abre menu, diálogo ou painelBotão com o estado do padrão: aria-expanded, popovertarget ou commandfor
Parece botão, mas navegaContinua link: só muda a classe
Parece link, mas executa açãoContinua botão: só muda a classe
É a ação principal da regiãoPrimário, um por região visível
É uma alternativa relevanteSecundário ou tonal
É auxiliar ou pouco usadaTerciário (texto), sem perder legibilidade
É destrutivaRótulo que diz a consequência; cor vermelha não basta
Aceitar e rejeitar cookiesMesmo peso visual e mesmo acesso para as duas
Várias ações relacionadasGrupo; 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.

Sete partes de um botão, do contêiner à área de toque

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 é.

Anatomia. O desenho tem 44 px de altura; a área de toque pode ser maior do que o desenho.
Anatomia de um botão em sete partes Um botão de 44 pixels de altura com ícone inicial, rótulo e ícone final. Ao redor, a área de toque tracejada de 48 pixels, o anel de foco com 3 pixels de deslocamento e as indicações: contêiner, rótulo, ícone inicial, ícone final, anel de foco, área de toque e estado. Enviar inscrição 1. Ícone inicial (decorativo) 2. Rótulo: verbo + objeto 3. Ícone final (direção) 4. Contêiner (fundo, borda, raio) 5. Anel de foco (3 px) 6. Área de toque (≥ 24, ideal 44 a 48) 7. Estado: padrão, hover, foco, pressionado, carregando, sucesso, erro, indisponível, desabilitado, selecionado
O que cada parte faz e o cuidado que ela pede
ParteFunçãoCuidado
ContêinerSuperfície, forma e limitemin-block-size e padding dão tamanho; nenhum deles muda a semântica
RótuloDiz a ação ou o destinoVerbo + objeto (“Salvar endereço”); um aria-label substitui o texto, então não pode contradizê-lo
Ícone inicialReconhecimento rápidoRedundante com o texto: aria-hidden="true" no SVG
Ícone finalDireção, expansão, destino externoA seta para baixo não substitui aria-expanded; o ícone de link externo não avisa o leitor de tela sozinho
Anel de focoOnde 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 toqueOnde o clique pegaPode passar do desenho com um pseudo-elemento; nunca pode invadir o vizinho
EstadoDisponível, ocupado, ligadoO visual, o atributo e o comportamento precisam dizer a mesma coisa

Uma tela, uma ação principal: a hierarquia em oito níveis

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.

Primário (preenchido)A ação que a tela existe para provocar. Contraste alto, um por região.
TonalSegunda ação forte, sem brigar com o primário. Popular no Material 3.
Secundário (contorno)Alternativa relevante. A borda precisa de 3:1 contra o fundo.
ElevadoSobre fundos com textura, a sombra separa o botão da superfície.
Terciário (texto)Ação auxiliar. Sublinhado ou cor de link ajudam a parecer clicável.
Fantasma (ghost)Transparente com borda neutra. Some fácil sobre foto: teste o contraste.
DestrutivoVermelho e um rótulo que diz o que se perde. Raramente é o primário da tela.
NeutroSair sem fazer nada. Em diálogo, fica ao lado do primário com o mesmo tamanho.
Antes: três primários na dobra

“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.

Depois: um primário, um secundário, um link

“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.

Dez estados que o botão precisa ter antes de ir ao ar

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.

Padrão<button>
Hover: só com ponteiro:hover
Foco visível:focus-visible
Pressionado (instante):active
Carregandoaria-busy="true"
Sucesso + aviso em role="status"data-estado
Erro com motivo ao ladoaria-describedby
Indisponível, mas focável e explicadoaria-disabled="true"
Desabilitado: sai do Tabdisabled
Selecionado / ligadoaria-pressed="true"
Matriz de estados: o que muda no visual, no atributo e no comportamento
EstadoVisualAtributo ou seletorComportamento
PadrãoContraste de 4,5:1 no textonenhumClicável, na ordem do Tab
HoverFundo 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ívelAnel de 2 a 3 px com 3:1 contra o fundo:focus-visibleNão muda a função; não pode ficar escondido atrás de barra fixa (2.4.11)
PressionadoEscala de 0,97 ou fundo mais denso:activeAção dispara ao soltar, para dar chance de desistir (2.5.2)
CarregandoIndicador girando + verbo no gerúndio, largura mantidaaria-busy="true" + aria-disabled="true"Ignora o segundo clique; continua com foco
SucessoÍcone de confirmação, cor de sucesso por 2 a 3 sMensagem em role="status"Volta ao padrão ou leva ao próximo passo
ErroContorno de alerta + ícone + textoaria-describedby apontando o motivoVolta a funcionar; não apaga o que a pessoa digitou
IndisponívelEsmaecido, legívelaria-disabled="true"Continua focável e explica o motivo; o script impede a ação
DesabilitadoEsmaecidodisabledSai do Tab e o leitor de tela pode pular; use só quando o motivo é óbvio
SelecionadoPreenchido ou com marcaaria-pressed, aria-checked, aria-current ou aria-selected, conforme o padrãoPersiste 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.

Ver o código estados.cssCSS · estados por atributo · 33 linhas
/* 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; }
}

24 px é o piso, 44 é o conforto e 48 dp é o Android

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.

Escala de cinco tamanhos32 e 36 px para tabelas e barras densas com mouse; 44 como padrão; 52 e 64 para a ação principal no celular. O de 32 px ganha área de toque invisível de 44 px por um pseudo-elemento.
Tamanho mínimo de alvo por referência (consulta em 12/11/2026)
ReferênciaMínimoUnidadeObservação
WCAG 2.2, 2.5.8 (AA)24 × 24px de CSSOu 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 × 44px de CSSMeta para telas de toque e públicos com mais dificuldade motora
Apple Human Interface Guidelines44 × 44ptVale para iPhone e iPad; o SwiftUI amplia a área de botões pequenos quando há espaço
Android e Material 348 × 48dpLocalMinimumInteractiveComponentSize garante 48 dp mesmo no botão de 32 dp
Material 3 Expressive (2025)32, 40, 56, 96 e 136dpAlturas 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.

Ver o código area-de-toque.cssCSS · 14 linhas
/* 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 rótulo é a promessa: verbo, objeto e nada de “clique aqui”

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”.

Rótulos fracos e a versão que diz o resultado
OndeFracoFortePor quê
Landing de cursoSaiba maisVer o conteúdo das 12 aulasDiz o que a pessoa vai encontrar
Formulário de orçamentoEnviarReceber o orçamento em 24 hPromete o resultado e o prazo
Teste grátisCadastre-seComeçar o teste de 14 diasTira o peso do cadastro e diz a duração
Diálogo de exclusãoOKExcluir 14 contatosDiz o que se perde, com número
CheckoutContinuarPagar R$ 149,90 com PixValor e meio de pagamento evitam surpresa
E-mail de confirmaçãoClique aquiConfirmar meu e-mailFunciona até sem a imagem e sem o texto em volta
Painel de softwareProcessarImportar 2.340 leadsVerbo do domínio e tamanho da tarefa
Botão de IAUsar IAResumir a conversaA 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 ajuda, rótulo explica: quando o desenho pode ficar sozinho

Í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.

Ícone + rótuloO SVG leva aria-hidden="true": o texto já dá o nome.
Só ícone, símbolo universalNome por aria-label; o desenho sozinho basta para quem enxerga.
Só ícone com dicaPasse o mouse ou o Tab. A dica usa interestfor quando o navegador tem e um script curto quando não tem.
Erro comum: só o 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.

Ver o código dica-por-interesse.htmlHTML · interestfor com reserva · 31 linhas
<!-- 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>

Botões de site e landing page: dezesseis peças que funcionam aqui

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

Chamada principal sobre imagemPrimário claro sobre fundo escuro e secundário de vidro. Sobre foto, meça o contraste no ponto mais claro da imagem, não na média.
WhatsAppÉ link para wa.me, com o número só em dígitos e a mensagem pronta no parâmetro text. Avise que abre fora.
PagamentoValor e meio no rótulo. O botão de pagar é type="submit" do formulário de checkout e ganha guarda contra clique duplo.
LoginChave de acesso (passkey) primeiro, provedor social depois, senha por último. A marca de terceiros segue o manual de cada um; aqui ficam ícones neutros.

CookiesAceitar e rejeitar com o mesmo peso visual e o mesmo número de cliques. Destacar só o aceitar é o padrão que órgãos de proteção de dados apontam como manipulação.
Cartão inteiro clicávelO link fica no título e estica a área com ::after; o botão “Salvar” fica por cima e continua independente. Nada de link dentro de link.

Retorno com View TransitionsO botão confirma no próprio lugar e volta. A transição é um enfeite: sem ela, a troca é imediata.

Curtir com contadorNome fixo (“Curtir”) e estado em aria-pressed; o número é visual e o anúncio diz o que mudou.

Compartilhar e copiarnavigator.share() abre a folha do sistema; sem ela, copia o link. O “Copiado” só aparece depois que a cópia deu certo.
  • Resultado 1
  • Resultado 2
  • Resultado 3

Carregar maisDiz quantos vêm, anuncia quantos chegaram e, no fim da lista, entrega o foco ao último item em vez de deixá-lo cair no nada.
Voltar ao topoLink para uma âncora que existe. O script só suaviza a rolagem quando o movimento está liberado e devolve o foco ao início.
Gradiente vivoO brilho corre duas vezes, só com movimento liberado, e para no estado final. Meça o contraste nas duas pontas do gradiente.

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.

R$ 390
ou 3× de R$ 130
Agendar a primeira sessão
Barra fixa no celularposition: 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).
Ver o código cartao-inteiro.cssCSS · link esticado · 15 linhas
/* 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; }
Ver o código barra-fixa.htmlHTML e CSS · celular e teclado virtual · 34 linhas
<!-- 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.

Botões de software: barras, divididos, menus e atalhos

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.

Barra de ferramentas com tabindex rotativoUm Tab entra na barra; as setas andam entre os botões; Home e End vão às pontas. Negrito e itálico são alternâncias com aria-pressed. Atalhos anunciados por aria-keyshortcuts.

Botão dividido (split button)Dois alvos: a ação padrão à esquerda e a seta que abre o menu. Menu em popover, posicionado por anchor-name quando o navegador tem.

Botão de menuSeta para baixo abre e foca o primeiro item; setas andam; Esc fecha e devolve o foco ao botão. aria-expanded diz se está aberto.
Cobrança
SegmentadoEscolha única entre poucas opções (no anual, dois meses saem de graça): rádios nativos com cara de botão. As setas trocam a opção e o leitor diz “1 de 2”.
Interruptor (switch)Efeito imediato, sem botão de salvar. role="switch" com aria-checked; o nome não muda com o estado.

AlternânciaMesmo nome nos dois estados; quem muda é o aria-pressed. Trocar o texto para “Ativar som” e manter o aria-pressed confunde.

StepperDois botões nomeados e um campo numérico de verdade, com min e max. Digitar 12 tem de funcionar.
Campanhas de novembro com ações por linha
CampanhaEnviosAções
Black Friday · aquecimento12.480
Reativação de inativos3.912
Ações por linhaCada botão diz de qual linha é (“Editar Reativação de inativos”), porque o leitor de tela pode listar todos os botões fora da tabela. 36 px com 4 px de vão passa no 2.5.8.
Comando com atalho visívelO <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.
Windows (WinUI 3)
macOS e Material
Ordem dos botões no diálogoWindows põe a ação principal primeiro; macOS e Material, por último, à direita. Na web, a ordem do DOM é a ordem do Tab: alinhar à direita com flex-direction: row-reverse deixa o Tab ao contrário do olho.
Ver o código barra-de-ferramentas.jsJavaScript · tabindex rotativo · 28 linhas
// 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"));
  });
}
Ver o código botao-dividido.htmlHTML e CSS · popover e âncora · 43 linhas
<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.

Botões de aplicativo: polegar, botão flutuante, gestos e o sistema

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.

Tarefas de hoje

Botão flutuante que abre ações (speed dial)O botão de criar fica no canto da mão, acima da barra de navegação. Abre a lista com 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.
Segure 1,2 s com o dedo, o mouse ou a tecla Espaço.

Segurar para confirmarPara ação rara e grave. Soltar antes cancela. Precisa de alternativa sem tempo (um diálogo de confirmação) para quem tem tremor ou usa acionador.

Deslizar para confirmarArrastar nunca pode ser o único caminho (critério 2.5.7). Aqui, Enter no botão e o link de baixo confirmam sem gesto.
O mesmo papel em cada plataforma (APIs conferidas na documentação em 12/11/2026)
PapeliOS 26 · SwiftUIAndroid · Compose Material 3Windows · WinUI 3Web
Primário.borderedProminent ou .glassProminentButtonAccentButtonStyle<button> preenchido
Secundário.bordered ou .glassFilledTonalButton, OutlinedButtonButtonContorno ou tonal
Texto.borderlessTextButtonHyperlinkButtonBotão com cara de link
Destrutivorole: .destructiveCores de erro do temaSem estilo próprio: cor do temaVariante vermelha + rótulo explícito
DivididoMenu com primaryActionSplitButtonLayout (Expressive)SplitButtonDois botões + popover
GrupoControlGroupButtonGroup (Expressive)CommandBarrole="group" ou role="toolbar"
FlutuanteBotão em .toolbar de vidroFloatingActionButton, FloatingActionButtonMenuNão é padrãoBotão fixo com aria-expanded
Retorno tátil.sensoryFeedback(.success, trigger:)performHapticFeedback(HapticFeedbackType.Confirm)Não se aplicanavigator.vibrate() só no Android; nunca o único sinal
Alvo mínimo44 × 44 pt48 × 48 dp32 px no mouse; 40 no toque24 px (AA), 44 (AAA)
Ver o código Botoes.swiftSwiftUI · iOS 26 · 35 linhas
import 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) { } }
        }
    }
}
Ver o código Botoes.ktJetpack Compose · Material 3 Expressive · 33 linhas
// 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.

Botões de e-mail: tabela, VML e o modo escuro

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.

Teste de estresse

O botão é texto num fundo de célula, então sobrevive sem imagem. Um botão que é imagem some com ela.

De: Clínica AuroraConfirme sua avaliação

Olá, Marina! Sua avaliação está marcada para sexta, 14 de novembro, às 15h.

Confirmar presença

Precisa remarcar? Escolher outro horário.

Ver o código botao-email.htmlHTML de e-mail · tabela, VML e modo escuro · 42 linhas
<!-- 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. -->
Como cada cliente trata o botão de e-mail (levantamento de 12/11/2026)
ClientePadding e raio no linkLê o bloco do Outlook (VML)Modo escuro
Outlook clássico (Windows)NãoSimInverte parte das cores
Novo Outlook (Windows) e Outlook.comSimNãoInversão parcial; marca com [data-ogsc] e [data-ogsb]
Gmail (web e aplicativo)SimNãoO aplicativo inverte por conta própria; ignora prefers-color-scheme
Apple Mail (macOS e iOS)SimNãoRespeita 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.

Formulários: submit, Enter, formaction e o clique duplo

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.

Um formulário, três botões, três comportamentos. Envie com cada um e com Enter no campo.

Atributos que o botão de formulário já tem de graça
AtributoO que fazUso típico
type="submit | button | reset"Envia, não faz nada sozinho ou volta aos valores iniciaisSempre escrito; reset quase nunca vale o risco
form="id"Liga o botão a um formulário que está em outro lugar da páginaBotão de salvar na barra fixa, fora do <form>
formaction, formmethodTroca destino e método só para este botão“Exportar” e “Salvar” no mesmo formulário
formnovalidateEnvia sem a validação nativa“Salvar rascunho” com campos incompletos
name + valueManda 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 returnValueConfirmar 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.

Botões de IA: dizer o resultado, deixar parar e mostrar o custo

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.

GerarVerbo do resultado e ícone de brilho como reforço.
PararNo mesmo lugar do botão que começou, enquanto o texto chega.
Ações depois da respostaNomes completos; o polegar é alternância com 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.

O que a plataforma web entregou para botões entre 2024 e 2026

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.

Botão que abre diálogo sem JavaScript: Invoker Commands

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.

Diálogo por atributocommand="show-modal"; os botões do diálogo usam request-close e value.
0
Comando própriocommand="--somar" dispara o evento command no <output>, que decide o que fazer.
Ver o código comandos.htmlHTML · commandfor com reserva · 32 linhas
<!-- 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>

Menu que se posiciona sozinho: popover e anchor positioning

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.

Dica que aparece no interesse: interestfor

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.

Cor que se calcula: contrast-color, light-dark e color-mix

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.

Entrada, saída e forma

@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.

Recursos para botões e o selo Baseline (Web Features Explorer e MDN, consulta de 12/11/2026)
RecursoPara que serve no botãoSituaçãoNo seu navegador
:focus-visible, :has()Anel de foco só no teclado; estilo do grupo pelo estado do filhoAmplamente disponível
PopoverMenu e painel na camada de cima, Esc e clique foraBaseline desde 27/01/2025
Invoker CommandsAbrir diálogo e popover por atributoBaseline desde 12/12/2025
requestClose()Fechar com chance de cancelar (alterações não salvas)Baseline desde 27/05/2025
closedbyDiálogo que fecha com clique foraLimitado: sem Safari estável
Anchor positioningMenu e dica presos ao botãoPropriedades centrais Baseline desde 13/01/2026
interestforDica no ponteiro e no focoLimitado: só Chromium
contrast-color()Texto branco ou preto automáticoBaseline desde 10/04/2026
light-dark()Cor por tema numa linhaBaseline desde 13/05/2024
View Transitions na mesma páginaBotão que vira outro estado com continuidadeBaseline desde 14/10/2025
@starting-styleEntrada animada de menu e popoverBaseline desde 06/08/2024
sibling-index()Atraso em cascata num grupo de botõesBaseline desde 18/08/2026
corner-shapeCanto squircleLimitado: só Chromium
text-boxRótulo centrado pela altura das letrasEm consolidação (a confirmar no agregado)
Select personalizável<button> dentro do <select> com appearance: base-selectLimitado: Chromium e Safari 27
Consultas de estado de rolagemBarra fixa que muda ao grudarLimitado: só Chromium
::scroll-button()Botões de rolar gerados pelo CSS num carrosselLimitado: só Chromium
if() no CSSValor condicional sem duplicar regraLimitado: 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.

Cor e tokens: a marca raramente passa sozinha no contraste

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.

Os três contrastes de um botão

Um botão precisa passar em três medições diferentes. Quem cuida só do texto esquece as outras duas.

Tokens em três camadas

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.

Tokens de componente do botão
TokenO que controla
--botao-fundoPreenchimento em repouso
--botao-textoRótulo e ícone, com 4,5:1 contra o fundo
--botao-bordaContorno, com 3:1 contra a página nas variantes sem preenchimento
--botao-hoverFundo com o cursor em cima
--botao-pressionadoFundo durante o clique ou toque
--botao-focoCor do anel de :focus-visible
--botao-desabilitadoFundo apagado de ação indisponível
--botao-perigoAções destrutivas, como excluir

Derivar estados no próprio CSS

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.

Alto contraste forçado

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.

Ver o código tokens-botao.cssCSS · tokens em camadas, estados e modos de contraste · 39 linhas
@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.

Forma e movimento: raio, squircle, pressão e o que desligar

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.

Squircle e as linguagens novas

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.

Tempos de movimento

Quanto dura cada movimento
MomentoDuração e propriedadePor quê
Pressão80 a 120 ms, transform: scale(.97)Confirma o toque sem atrasar a ação
Hover150 ms, opacity de uma camadaSuaviza a troca sem parecer lento
Entrada de menu160 a 200 ms, @starting-styleMostra de onde o menu veio
Sucesso300 msDá tempo de notar que deu certo
Limite500 msAcima 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.

Antes

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.

Depois

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.

Ver o código movimento-botao.cssCSS · pressão, hover, entrada de menu e movimento reduzido · 40 linhas
.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; }
}

Do HTML ao componente: CSS puro, Tailwind 4 e React 19 no mesmo contrato

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.

O CSS pode expressar as variantes por atributos ou classes

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.

Ver o código botao.cssCSS · base, variantes, foco e estados · 66 linhas
@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;
  }
}

O elemento precisa acompanhar a intenção da ação

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.

Antes

“Ver coleção” da Moda Alba usa um botão com redirecionamento por JavaScript. O visitante perde comportamentos próprios de um link.

Depois

“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.

O Tailwind 4 transforma o contrato em utilitários

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.

Ver o código botao-tailwind.cssCSS e HTML · tema, utilitário e uso no HTML · 40 linhas
@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 React mantém semântica e estado no mesmo contrato

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.

Ver o código Botao.tsxReact 19 e TypeScript · link ou ação com estado de carregamento · 77 linhas
import 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.

Cada camada assume uma responsabilidade verificável

Distribuição de responsabilidades do componente
ResponsabilidadeCSS puroTailwindReact
EstiloTokens e seletores.Tema, utilitários e variantes.Seleciona classes ou atributos.
SemânticaDepende do HTML.Depende do HTML.Escolhe link ou botão.
EstadoRepresenta atributos.Aplica variantes condicionais.Coordena dados e bloqueia ações.
TecladoExibe foco.Estiliza o foco.Preserva comportamento nativo ou usa biblioteca.
TesteContraste 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.

Tendências de 2026: vidro, Expressive, gradiente vivo e o que fica

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.

Liquid Glass: o vidro da Apple

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.

Ver o código botao-vidro.cssCSS · vidro com reserva opaca e respeito às preferências · 20 linhas
.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;
  }
}

Material 3 Expressive: tamanhos e formas novos

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.

Botões de IA com variante própria

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.

Gradiente vivo com @property

O @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.

Ver o código gradiente-vivo.cssCSS · gradiente animado e movimento reduzido · 27 linhas
@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; }
}

Login, pagamento e desfazer

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ências de 2026 e seus cuidados
TendênciaQuando usarCuidado
Vidro translúcidoBotões flutuantes sobre fotos ou mapasContraste variável; reserva opaca e prefers-reduced-transparency
Material 3 ExpressiveApps Android e telas com uma ação de destaqueTamanhos L e XL ocupam muito espaço; reserve para a ação principal
Botão de IAAções que geram conteúdoRótulo com verbo claro; o brilho sozinho não explica a ação
Gradiente vivoUma chamada principal por telaAnimar só background-position e parar com movimento reduzido
Chave de acesso e Pix no rótuloLogin e checkoutManter alternativa de senha e mostrar o valor exato
Desfazer no lugar de confirmarArquivar, remover, moverPrazo 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.

Como catorze sistemas de design desenham o botão

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.

Variantes, alturas, raio, botão dividido, grupo e estado de carregamento em catorze sistemas
SistemaVariantesAlturasRaioDividido e grupoCarregando
Material 3 (com Expressive)elevated, filled, tonal, outlined, text; o Expressive acrescenta as formas round e squareM3 básico: S 40 dp. Expressive: XS 32, S 40, M 56, L 96, XL 136 dpround: totalmente arredondado; square: 12, 12, 16, 28, 28 dpNo Expressive, SplitButton e ButtonGroup; o M3 básico não traz dividido como componenteSem estado nativo; compor indicador
Apple HIG, Liquid Glass, iOS 26bordered, borderedProminent, borderless, plain, glass, glassProminentA confirmar; dependem do estilo e do contexto. Área de toque recomendada: 44 × 44 ptCápsula nos botões com borda; varia conforme o contextoDividido a confirmar; agrupamento por layoutEm UIKit, UIButton.Configuration.showsActivityIndicator; desativação é separada
Microsoft Fluent 2 (React v9)primary, secondary, outline, subtle, transparentsmall 24, medium 32, large 40 px4 px no padrão; também circular e squareSplitButton; grupo por composição em barra de ferramentasAusente no Button v9; compor Spinner
IBM Carbon v11primary, secondary, tertiary, ghost, danger, danger--tertiary, danger--ghostXS 24, S 32, M 40, L 48, XL 64 px; 2XL 80 px na documentação atual, disponibilidade em v11 a confirmar0 px no padrão; arredondamento da v12 é opcional na v11ComboButton (disponibilidade em v11 a confirmar); ButtonSetCompor InlineLoading; suporte contextual em modal
Shopify Polaris (App Home)auto, primary, secondary, tertiary; tone à parteA confirmar; dependem da superfícieA confirmarDividido a confirmar; s-button-grouploading substitui o conteúdo e desativa o botão
Atlassian Design Systemdefault, primary, subtle, warning, danger, discovery, rovodefault 32, compact 24 px6 px no padrão; 4 px no compactoSplitButton e ButtonGroupisLoading
GitHub Primerdefault, primary, danger, invisible, linksmall 28, medium 32, large 40 px6 px (--borderRadius-medium)Dividido por composição com ButtonGroup e menu; ButtonGrouploading, que preserva o foco
GOV.UK Design Systemdefault, start, secondary, warning, inverseSem escala fixa; cerca de 38 px numa linha com fonte de 19 px0 pxDividido não documentado; govuk-button-groupAusente; data-prevent-double-click evita clique duplo, sem indicador
Adobe Spectrum 2 (React Spectrum S2)primary, secondary, accent, negative, premium, genai; fillStyle: fill, outlineS, M, L, XL; valores em px a confirmarCápsula: metade da alturaDividido a confirmar; ButtonGroupisPending; spinner aparece após 1 segundo
Salesforce SLDS 2 (Lightning Web Components)base, neutral, brand, brand-outline, destructive, destructive-text, inverse, successBotão de texto a confirmar; lightning-button-icon: 16, 20, 24, 32 pxA confirmarDividido por composição de lightning-button e lightning-button-menu; lightning-button-groupNão documentado no lightning-button; is-loading no lightning-button-menu
shadcn/ui (new-york-v4)default, secondary, destructive, outline, ghost, linkxs 24, sm 32, default 36, lg 40 pxrounded-md; 8 px no tema de exemplo, configurávelDividido por composição com ButtonGroup; ButtonGroupSem prop; compor Spinner e disabled
Radix Themesclassic, solid, soft, surface, outline, ghost1 24, 2 32, 3 40, 4 48 pxConfigurável: none, small, medium, large, fullAmbos por composição, inclusive Toolbarloading mostra spinner e desativa
Base UISem variantes visuais: primitiva sem estiloDefinidas pela aplicaçãoDefinido pela aplicaçãoDividido por composição; Toolbar.GroupSem prop; usar disabled e focusableWhenDisabled
React Aria ComponentsSem variantes visuais: primitiva sem estiloDefinidas pela aplicaçãoDefinido pela aplicaçãoPor composição; ButtonGroup a confirmarisPending cuida do comportamento e do anúncio; o indicador visual é composto à parte

Seis regras que os sistemas escrevem

  1. Material 3: rótulos breves, de preferência com 1 a 3 palavras, em sentence case (só a primeira letra maiúscula), sem truncar nem quebrar linha.
  2. Carbon: prefira verbo + substantivo; se o rótulo não couber, deixe ir para uma segunda linha em vez de cortá-lo.
  3. GOV.UK: sentence case e rótulo que descreve a ação; "Continue" quando nada é salvo e "Save and continue" quando há salvamento.
  4. Atlassian: use o botão dividido para ações diretamente relacionadas quando houver uma ação principal clara; sem ação padrão evidente, prefira menu ou grupo.
  5. Primer: nunca mais de um botão primary no mesmo grupo, e raramente mais de um por página.
  6. Base UI: não use 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.

O que dá para concluir

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.

O que a pesquisa diz sobre botão e conversão (e o que é lenda)

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.

Estudos com método

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.

Testes relatados por empresas

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.

Testes A/B de botão relatados por empresas e o que cada um deixa em aberto
TesteO que mudouResultado relatadoRessalva
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 outroOs 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 semanasAmostra 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 diasMediu 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 visitasSignificâ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 significativaO 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õesTexto, 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 receitaRedesenho e corte de links aconteceram juntos; amostra e duração não informadas

Orientações

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.

Lenda

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.

O que dá para afirmar

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.

Acessibilidade: doze armadilhas e o critério da WCAG 2.2 de cada uma

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.

Doze armadilhas, o critério da WCAG 2.2 e a correção prática
ArmadilhaCritérioCorreção
Ícone sem nome, papel errado ou estado de alternância não exposto.4.1.2 — Nome, função, valor — APreferir 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 — APermitir 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 — AADefinir 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) — AAGarantir 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) — AAExigir 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 — AAUsar 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) — AABuscar 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 — AAOferecer 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 — AIncluir 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 — APreferir 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 — AAUsar 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 — APermitir desligar, ajustar ou estender o limite conforme o critério, ou fornecer recuperação sem prazo; observar suas exceções.

O teste de dois minutos que pega quase tudo

  1. Tab do começo ao fim. Todo botão recebe foco, o anel aparece, nada fica escondido atrás de barra fixa e a ordem segue a leitura.
  2. Enter e Espaço em cada botão. Os dois acionam o botão; no link, só Enter.
  3. Leitor de tela na lista de botões. NVDA (Insert+F7) ou VoiceOver (rotor): cada nome faz sentido fora do contexto e nenhum se repete à toa.
  4. Zoom de 200% e largura de 320 px. Nenhum rótulo corta, nenhum botão some, nada exige rolagem lateral.
  5. Alto contraste forçado e movimento reduzido. No DevTools, emule 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.

Oficina: encolha até reprovar

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.

Cenário de exercício

Quem
Carla, designer da Mirante, e o senhor Joaquim, de 71 anos, que confirma consultas da Clínica Aurora pelo celular.
Tarefa
Revisar a barra de ações do lembrete de consulta: confirmar, remarcar, ligar, mapa e cancelar.
Falha vista
O senhor Joaquim cancelou a consulta ao tentar abrir o mapa. Os ícones têm 20 px e 2 px de vão.
Resultado esperado
Cada alvo com 24 px ou mais (ideal 44 no celular), ou com espaço suficiente para o círculo de 24 px, e o cancelamento longe dos outros.
Mapa da oficina. Medir, testar o círculo, ampliar sem invadir, conferir no dedo.
1. MedirgetBoundingClientRect de cada alvo.
2. Círculo24 px centrado nos alvos pequenos.
3. AmpliarPseudo-elemento de 44 px sem sobrepor.
4. PolegarTeste num aparelho real, com uma mão.

  1. Carla mede: 20 × 20 px cada ícone, centros a 22 px um do outro.
  2. Os círculos de 24 px se cruzam: os cinco reprovam no 2.5.8. Com 4 px de vão eles só se tocariam, e aí passariam, no limite.
  3. Ela amplia a área para 44 px, mas com 2 px de vão as áreas se cruzam; sobe o vão para 24 px.
  4. No celular do senhor Joaquim, o mapa abre na primeira tentativa e o cancelar fica isolado no fim.

Sem JavaScript, o quadro aparece completo; a lista numerada descreve cada passo.

Demonstração: lado, vão e área ampliada

Os cinco alvos reprovam no critério 2.5.8.

    Investigar as falhas

    Falhas de alvo de toque, com o sintoma e a correção
    SintomaCausa provávelComo confirmarCorreção
    Toca um, aciona o vizinhoAlvos menores que 24 px com vão curtoCírculos de 24 px se cruzamAumente o lado ou o vão
    Área ampliada piorouPseudo-elementos de 44 px se sobrepõemContorno no ::after no DevToolsVão de pelo menos 44 menos o lado
    Passa no desktop, falha no celularTamanho em rem com fonte menor no celularMeça a 390 px de larguramin-block-size: 44px fixo
    Ação destrutiva cancelada sem quererCancelar colado em ação frequenteOrdem dos botões e distânciaIsolar no fim, com desfazer
    Link de texto reprova na auditoriaFerramenta não aplicou a exceção de texto em linhaO link está dentro de uma frase?Exceção válida; registre a justificativa

    Código de referência

    Ver o código medir-alvos.jsJavaScript · teste do círculo de 24 px · 28 linhas
    // 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),
    })));

    Critérios de aceite

    Oficina: clique duas vezes e veja o estrago

    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.

    Cenário de exercício

    Quem
    Renata, gestora de tráfego da Mirante, e o CRM do cliente Escola Ritmo (fictícia), que recebe as inscrições da aula experimental.
    Tarefa
    Descobrir por que 9 em cada 100 inscrições de outubro de 2026 chegaram em dobro no CRM.
    Falha vista
    No 4G, a resposta demora 2 s; quem clica de novo cria outra inscrição e recebe dois e-mails de boas-vindas.
    Resultado esperado
    Um envio por intenção, foco preservado, largura estável, aviso de “Enviando…” e erro que devolve o botão com o motivo.
    Mapa da oficina. Guardar, avisar, manter o lugar e devolver.
    1. GuardaSegundo envio ignorado enquanto o primeiro corre.
    2. Avisoaria-busy e “Enviando…” em região de status.
    3. LugarFoco no botão e largura fixa.
    4. VoltaSucesso ou erro com o motivo ao lado.

    1. O primeiro clique liga a guarda: enquanto a resposta não volta, outro envio não sai.
    2. O botão mostra o indicador e “Enviando…”, e a região de status anuncia o mesmo para o leitor de tela.
    3. O foco fica no botão, que mantém a largura com min-inline-size; nada ao lado pula.
    4. Com sucesso, “Inscrição enviada”; com erro, “Tentar de novo” e o motivo ligado por aria-describedby.

    Sem JavaScript, o quadro aparece completo; a lista numerada descreve cada passo.

    Demonstração: latência, estratégia e falha de rede

    Estratégia contra o segundo clique

    Requisições enviadas: 0

    Inscrição na aula experimental · Escola Ritmo

    Registro

      Investigar as falhas

      Falhas de envio, com o sintoma e a correção
      SintomaCausa provávelComo confirmarCorreção
      Inscrição em dobro no CRMNenhuma guarda contra o segundo envioDuas requisições na aba Rede com 1 s de intervaloGuarda no evento submit e chave de idempotência no servidor
      O leitor de tela fica mudodisabled tira o foco do botãodocument.activeElement vira bodyaria-disabled + guarda no script
      O layout pula ao enviar“Enviar” vira “Enviando…” e o botão cresceLargura antes e depois no registromin-inline-size do maior rótulo
      Erro some sem explicaçãoMensagem só em cor ou só no consoleSimule a queda de redeTexto do motivo + aria-describedby + role="status"
      Formulário apagado depois do erroRecarregar a página para “resetar”Os campos voltam vaziosManter os valores e só devolver o botão

      Código de referência

      Ver o código envio-seguro.jsJavaScript · guarda, aviso e retorno · 40 linhas
      // 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");
        }
      });
      Ver o código BotaoEnviar.tsxReact 19 · useFormStatus · 31 linhas
      "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>

      Critérios de aceite

      Oficina: a cor da marca nos quatro estados e nos dois temas

      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.

      Cenário de exercício

      Quem
      Bruno, dono da Moda Alba (cliente fictícia), que quer o botão “exatamente no rosa da marca”, e Carla, designer da Mirante.
      Tarefa
      Levar o rosa da marca para os botões da loja nos temas claro e escuro sem reprovar no contraste.
      Falha vista
      Com texto branco, o rosa dá 4,0:1; com texto quase preto, 4,2:1. Nenhum dos dois chega a 4,5:1, e o hover, mais claro, piora.
      Resultado esperado
      Texto com 4,5:1 em padrão, hover e pressionado; botão com 3:1 contra a página; anel de foco com 3:1; um token por estado.
      Mapa da oficina. Cor de marca, texto, estados derivados e o tema escuro.
      1. MarcaA cor que o cliente pediu.
      2. TextoBranco, escuro ou contrast-color.
      3. EstadosHover e pressionado por color-mix.
      4. EscuroClarear até separar do fundo.

      1. Carla parte do rosa que Bruno mandou e mede contra o branco do site: o botão aparece, mas o texto não.
      2. Testa texto branco e quase preto: nenhum passa. A saída é escurecer o rosa só no botão, até o branco chegar a 4,5:1.
      3. Hover e pressionado saem da marca por color-mix, escurecendo no claro e clareando no escuro.
      4. No escuro, o rosa clareia até ter contraste com o fundo; os dois valores vão num só light-dark().

      Sem JavaScript, o quadro aparece completo; a lista numerada descreve cada passo.

      Demonstração: escolha a cor e o texto

      Calculando…

      Tema claro
      Tema escuro
      /* os tokens aparecem aqui */

      Investigar as falhas

      Falhas de cor em botão, com o sintoma e a correção
      SintomaCausa provávelComo confirmarCorreção
      Texto branco “apagado”Cor de marca de luminosidade médiaContraste abaixo de 4,5:1Texto escuro ou marca mais escura só no botão
      Hover pior que o padrãoHover clareia uma cor que já estava no limiteMeça o estado hover forçado no DevToolsNo claro, o hover escurece
      Botão some no tema escuroA mesma cor nos dois temasBotão contra a página abaixo de 3:1light-dark() com a versão clara
      Anel de foco invisívelAnel na cor do botão sobre fundo parecidoAnel contra a página abaixo de 3:1Anel duplo (cor + halo do fundo)
      Desabilitado ilegívelopacity: .4 no botão inteiroTexto abaixo de 3:1Cores próprias de desabilitado, legíveis

      Código de referência

      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.

      Ver o código contraste.jsJavaScript · 12 linhas
      // 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)

      Critérios de aceite

      Oficina: gere, pare e gere outra versão

      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.

      Cenário de exercício

      Quem
      Patrícia, recepcionista da Clínica Aurora (fictícia), que atende 80 conversas de WhatsApp por dia, e a equipe de produto da Mirante.
      Tarefa
      Desenhar o botão que resume a conversa antes de Patrícia responder.
      Falha vista
      A primeira versão dizia “Usar IA”, não tinha como parar e consumia crédito a cada clique sem avisar; Patrícia clicava três vezes até gostar.
      Resultado esperado
      Rótulo pelo resultado, parar no mesmo lugar, aviso de conteúdo gerado, fim anunciado e saldo de créditos visível.
      Mapa da oficina. Prometer, acompanhar, interromper e revisar.
      1. PromessaRótulo pelo resultado e custo antes do clique.
      2. AndamentoTexto chegando e região viva.
      3. PararNo mesmo lugar do botão.
      4. RevisarAviso, copiar e gerar outra versão.

      1. O botão diz “Resumir a conversa” e, ao lado, “1 crédito · restam 20”.
      2. O resumo chega palavra por palavra; a região viva anuncia “Gerando o resumo” uma vez, sem ler cada palavra.
      3. O botão vira “Parar” no mesmo lugar; Patrícia interrompe se o começo já basta.
      4. No fim, o aviso diz que o texto foi gerado e pede revisão; “Copiar” e “Gerar outra versão” aparecem.

      Sem JavaScript, o quadro aparece completo; a lista numerada descreve cada passo.

      Demonstração: ligue e desligue cada prática

      Cada resumo usa 1 crédito · restam 20

      O resumo aparece aqui.

      Texto gerado por IA a partir da conversa. Revise antes de enviar.

      Investigar as falhas

      Falhas de botão de IA, com o sintoma e a correção
      SintomaCausa provávelComo confirmarCorreção
      Ninguém sabe o que o botão fazRótulo pela tecnologia (“Usar IA”)Pergunte a três pessoas o que vai acontecerVerbo do resultado
      A pessoa fica refém da geraçãoSem pararTente interromper com o tecladoParar no mesmo lugar e AbortController na requisição
      Leitor de tela lê cada palavraRegião viva na caixa que recebe o texto em pedaçosAtive o leitor e gereAnuncie início e fim numa região de status separada
      Cliques compulsivos em “gerar de novo”Custo invisívelConsumo de crédito por sessãoCusto e saldo ao lado do botão
      Texto gerado enviado como se fosse da pessoaSem aviso nem revisãoProcure a marca de conteúdo geradoAviso visível e passo de revisão antes de usar

      Código de referência

      Ver o código botao-ia.jsJavaScript · fluxo com AbortController · 36 linhas
      // 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.

      Critérios de aceite

      Prompts para pedir botões a um agente de código

      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.

      Cinco pedidos prontos para colar

      Ver o código prompt-1.mdPrompt · componente de botão a partir dos tokens · 17 linhas
      Você é 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.
      Ver o código prompt-2.mdPrompt · revisão de landing page · 14 linhas
      Você é 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.
      Ver o código prompt-3.mdPrompt · envio com guarda de clique duplo · 13 linhas
      Você é 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.
      Ver o código prompt-4.mdPrompt · botão dividido com popover · 13 linhas
      Você é 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.
      Ver o código prompt-5.mdPrompt · botão de e-mail para Outlook · 13 linhas
      Você é 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.

      Três portões antes de aceitar o código

      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".

      Antes de publicar: a conferência em doze itens

      A lista leva uns vinte minutos por página. Cada item diz o que medir e onde.

      1. Elemento certo: Tab chega ao controle e o painel Acessibilidade mostra papel link ou button coerente com a ação.
      2. Nome acessível contém o texto visível, conferido no mesmo painel.
      3. Alvo de pelo menos 24 px a 390 px de largura, medido com getBoundingClientRect(); 44 px nos primários de celular.
      4. Contraste do texto de 4,5:1 e do contorno de 3:1, medidos no tema claro e no escuro.
      5. Anel de foco visível em todos os estados e sem ficar escondido por barra fixa (scroll-padding resolve).
      6. Estados carregando, erro e sucesso testados com rede lenta do DevTools.
      7. Clique duplo rápido gera uma requisição só, contada na aba Network.
      8. Teclado completo: Enter e Espaço no botão, Enter no link, Esc em menus e diálogos.
      9. Movimento reduzido: com prefers-reduced-motion emulado, nada pula nem gira sem necessidade.
      10. Alto contraste forçado: com forced-colors emulado, bordas e foco continuam visíveis.
      11. Impressão: a prévia de impressão esconde botões de ação e mantém o endereço dos links importantes.
      12. Peso: CSS e JS do botão somados em kB, anotados no PR.
      Ferramenta e o que ela mede
      FerramentaO que mede
      axe DevTools e @axe-core/playwrightViolações automáticas: nome ausente, contraste, papel incorreto
      LighthouseNota de acessibilidade e desempenho, peso de scripts
      ImpeccablePadrões visuais fracos com npx impeccable detect --json, repetido com --viewport 390x844
      Painel Acessibilidade do DevToolsPapel, nome acessível e estados de cada elemento
      Emulação no DevToolsComportamento com prefers-reduced-motion e forced-colors
      PlaywrightTab, Enter e Espaço em roteiro repetível, contagem de requisições
      Litmus ou Email on AcidBotões de e-mail em Outlook, Gmail e Apple Mail, claro e escuro

      O que vai no PR

      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.

      Por onde começar hoje, com os botões que já existem

      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.

      1. Inventariar: rode o seletor document.querySelectorAll('a, button, [role=button], [onclick]') no console e conte quantos há de cada tipo.
      2. Trocar toda div clicável por <button> e todo href="#" por um destino real ou por um botão.
      3. Reescrever rótulos fracos como "Clique aqui" e "Enviar" com verbo e objeto.
      4. Deixar um primário por região e rebaixar o resto para secundário ou terciário.
      5. Medir alvo e contraste na bancada da primeira tela.
      6. Tratar o envio: estado carregando, erro com motivo e guarda contra clique duplo.
      Ver o código inventario.jsJavaScript · conta os controles da página · 9 linhas
      const 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.

      Fontes para conferir e aprofundar

      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.

      Voltar aos materiais de Design