Teclado do celular cobrindo o botão de enviar: por que acontece e como resolver
Entenda viewport visual e de layout, tire o botão de enviar de position: fixed quando não precisa, use visualViewport quando precisa e teste com o teclado de verdade.
O formulário de agendamento cabe inteiro na tela do celular: nome, telefone, data e o botão verde "Agendar" preso no rodapé. A pessoa toca no telefone, o teclado sobe, e o botão some atrás dele. Ela rola, o botão não aparece. Fecha o teclado, o botão volta. Muita gente desiste nesse ponto achando que o formulário está quebrado.
A resposta curta: na configuração padrão dos navegadores móveis atuais, o teclado se sobrepõe à página sem redimensionar o layout, e um botão com position: fixed; bottom: 0 fica embaixo dele. A solução mais robusta é a mais simples: coloque o botão no fluxo normal, logo depois do último campo, e deixe espaço no fim da página para rolar. Se o design exige barra fixa, posicione-a com a API visualViewport.
Por que o teclado esconde o botão
O navegador móvel trabalha com duas "janelas". O viewport de layout é a área onde a página é montada, e é a ele que position: fixed se prende. O viewport visual é o pedaço que a pessoa realmente vê, que muda com zoom e com o teclado.
Quando o teclado abre, o viewport visual encolhe, mas o de layout continua do mesmo tamanho. O seu rodapé fixo segue preso ao fundo do layout, que agora está atrás do teclado. O Safari do iPhone sempre funcionou assim. O Chrome no Android passou a seguir o mesmo comportamento por padrão a partir da versão 108, e oferece uma opção para voltar a redimensionar o layout, vista mais abaixo.
Outro detalhe que engana: unidades como 100vh e as mais novas 100dvh acompanham a barra de endereço do navegador, não o teclado, na configuração padrão. Um formulário com height: 100dvh e botão no fundo continua com o botão escondido.
O caminho simples: botão no fluxo
- Tire o botão de enviar de
position: fixede coloque-o logo depois do último campo, dentro doform. - Use um
button type="submit"de verdade. Assim, a tecla de ação do próprio teclado envia o formulário, mesmo com o botão fora da tela. - Ajuste a tecla de ação com
enterkeyhint: "next" nos campos intermediários e "send" ou "done" no último, conforme o caso. - Deixe espaço no fim da página com
padding-bottom, para o último campo e o botão conseguirem rolar para cima do teclado. - Respeite a área segura dos aparelhos com barra de gestos, usando
env(safe-area-inset-bottom).
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<form action="/agendar" method="post" class="form-agendamento">
<label for="nome">Nome</label>
<input id="nome" name="nome" autocomplete="name" enterkeyhint="next">
<label for="tel">Telefone</label>
<input id="tel" name="tel" type="tel" autocomplete="tel" enterkeyhint="next">
<label for="data">Data</label>
<input id="data" name="data" type="date">
<button type="submit">Agendar</button>
</form>
.form-agendamento {
padding: 1rem 1rem calc(2rem + env(safe-area-inset-bottom));
}
.form-agendamento button {
width: 100%;
min-height: 48px;
margin-top: 1.5rem;
}
O env(safe-area-inset-bottom) só devolve um valor diferente de zero quando o meta viewport tem viewport-fit=cover. Ele protege contra a barra de gestos do aparelho, não contra o teclado. São dois problemas diferentes que costumam aparecer juntos.
Repare no enterkeyhint="next". Ele muda o rótulo da tecla, mas não move o foco sozinho: avançar para o próximo campo continua sendo trabalho do navegador ou do seu script. O ganho real é que, com form e botão de envio, a pessoa consegue enviar pela tecla do teclado mesmo sem ver o botão.
Quando a barra fixa é obrigatória
Em telas como um chat ou um carrinho com total sempre visível, a barra de ação precisa acompanhar o teclado. Você tem duas ferramentas.
Acompanhar o viewport visual com JavaScript
A API visualViewport informa altura e deslocamento da área visível e dispara eventos quando eles mudam. Com ela, você calcula quanto do fundo do layout está coberto e sobe a barra na mesma medida:
const vv = window.visualViewport;
function ajustarBarra() {
const alturaLayout = document.documentElement.clientHeight;
const coberto = alturaLayout - (vv.height + vv.offsetTop);
document.documentElement.style.setProperty('--coberto', Math.max(0, coberto) + 'px');
}
if (vv) {
vv.addEventListener('resize', ajustarBarra);
vv.addEventListener('scroll', ajustarBarra);
ajustarBarra();
}
.barra-acao {
position: fixed;
left: 0;
right: 0;
bottom: var(--coberto, 0px);
padding-bottom: env(safe-area-inset-bottom);
}
Trate esse código como ponto de partida e teste nos aparelhos que importam para você: a animação do teclado e a rolagem automática variam entre sistemas e versões. Também é bom dar ao conteúdo um padding-bottom igual à altura da barra, para ela não esconder o último campo.
Pedir ao navegador que redimensione o layout
O Chrome e outros navegadores baseados nele aceitam a chave interactive-widget=resizes-content no meta viewport. Com ela, o teclado encolhe o viewport de layout e o rodapé fixo sobe sozinho. O suporte não é universal, e o Safari do iPhone é o caso mais importante a conferir antes de depender disso. Use como melhoria, mantendo o caminho do botão no fluxo funcionando.
O campo focado também precisa aparecer
O navegador costuma rolar até o campo focado, mas um cabeçalho fixo no topo pode esconder exatamente esse campo. A propriedade scroll-padding-top no elemento que rola compensa a altura do cabeçalho, como mostra menu fixo que não cobre o título. Se o campo surge mas a página dá um pulo de zoom ao mesmo tempo, o culpado é outro e está em zoom ao tocar no campo.
Formulário curto também ajuda: cada campo a menos é um campo a menos atrás do teclado. Formulário que não cansa mostra como enxugar sem perder dado necessário.
Como testar de verdade
Simulador de tela alta e modo responsivo do desktop não abrem teclado virtual, então mentem. O teste precisa de um aparelho, de preferência um com tela pequena, porque é onde sobra menos espaço. Abra cada formulário, toque no último campo, digite e tente enviar sem fechar o teclado. Repita com o aparelho na horizontal, onde o teclado ocupa mais da metade da tela.
Erros comuns
- Botão de enviar com
position: fixed; bottom: 0. Com o teclado aberto, ele fica escondido e a pessoa acha que não há como enviar. - Formulário sem elemento
formou sem botãosubmit. A tecla de ação do teclado não envia nada e o único caminho é o botão escondido. - Usar
100vhou100dvhesperando que acompanhe o teclado. Na configuração padrão, essas unidades ignoram o teclado. - Confundir área segura com teclado.
env(safe-area-inset-bottom)resolve a barra de gestos, não o teclado. - Depender só de
interactive-widget. Em navegadores sem suporte, o botão continua escondido. - Testar só no modo responsivo do desktop. Sem teclado virtual, o problema não aparece até o cliente reclamar.
Checklist
- O botão de enviar está no fluxo, depois do último campo, salvo quando há motivo forte para barra fixa.
- Os campos estão dentro de um
formcombutton type="submit". - Os campos usam
enterkeyhintcoerente com a ação. - A página tem espaço no fim para o último campo rolar acima do teclado.
- Barras fixas acompanham o viewport visual e foram testadas em aparelho.
- Cabeçalho fixo não esconde o campo focado.
- O teste foi feito com teclado real, em retrato e paisagem.
Abra seu formulário mais longo num celular pequeno, toque no último campo e digite algo. O botão de enviar continua alcançável sem você fechar o teclado?
Referência: https://developer.mozilla.org/docs/Web/CSS/env