Teclado do celular cobre o campo ou o botão fixo? Como resolver no iPhone e no Android
Por que o teclado do celular esconde o campo ou o botão de enviar, por que o iPhone e o Android erram de jeitos diferentes e a correção para cada um.

Resposta curta: quando o teclado sobe, o iPhone e o Android mudam coisas diferentes na página. No iPhone, a página continua do mesmo tamanho e só a área visível encolhe, então um rodapé position: fixed fica atrás do teclado. No Android, com interactive-widget=resizes-content, a página encolhe de verdade e o rodapé fixo sobe junto com o teclado, e pode parar em cima do campo. A correção é escolher um comportamento na meta viewport e acompanhar a altura do teclado com visualViewport onde ele não é seguido.
Os dois viewports
O navegador do celular tem duas medidas da tela:
- Layout viewport: o tamanho da página. É por ele que o
position: fixed, o100vh, o100dvh, oinnerHeighte as media queries de altura se guiam. - Visual viewport: a parte que a pessoa enxerga agora. Encolhe com o zoom de pinça e com o teclado. O JavaScript lê em
window.visualViewport.
O teclado sempre encolhe o visual viewport. A pergunta é se ele encolhe o layout viewport também, e cada sistema responde de um jeito.
No iPhone: o rodapé fica atrás do teclado
O iOS nunca muda o layout viewport por causa do teclado, em nenhum navegador (todos usam o WebKit). innerHeight e 100dvh continuam iguais, só visualViewport.height diminui. O resultado:
- O rodapé ou a barra de abas com
bottom: 0fica escondido atrás do teclado. - O botão Enviar que mora num rodapé fixo some enquanto a pessoa digita.
- O iOS rola a página para o campo focado aparecer, mas só se a página tiver para onde rolar. Em layouts de app, com
body { overflow: hidden }e altura fixa, ele empurra a tela inteira para cima, junto com o cabeçalho fixo.
O iOS ignora o interactive-widget. Não adianta mudar a meta viewport para ele.
No Android: depende do interactive-widget
Desde o Chrome 108, o padrão no Android é o mesmo do iPhone: o teclado só encolhe o visual viewport. A meta viewport muda isso:
<meta name="viewport" content="width=device-width, initial-scale=1, interactive-widget=resizes-content">
| Valor | O que o teclado faz |
|---|---|
resizes-visual (padrão) |
Encolhe só a área visível, como no iPhone. O rodapé fixo fica atrás do teclado. |
resizes-content |
Encolhe a página. innerHeight, 100dvh e as media queries de altura mudam, e o rodapé fixo sobe até o topo do teclado. |
overlays-content |
Não muda nada. O teclado só cobre a página. |
O resizes-content é o que quase todo app de conversa quer: a caixa de mensagem gruda no teclado. Mas ele cria o defeito clássico. Se o que está fixo embaixo é a barra de abas e não a caixa de mensagem, a barra sobe com o teclado e senta em cima do campo e do botão Enviar. Parado, está tudo certo. Na hora de digitar, a pessoa não vê o que escreve.
A correção
1. Decida o que fica fixo embaixo
Com o teclado aberto, só o que a pessoa precisa para digitar deve ficar colado no teclado. Barra de abas, banner e botão flutuante saem do caminho:
@media (pointer: coarse) {
body:has(input:focus, textarea:focus, [contenteditable]:focus) .tabs {
display: none;
}
}
Isso resolve o Android com resizes-content e não atrapalha o iPhone, onde a barra estaria atrás do teclado de qualquer jeito. Evite esconder pela altura da tela (max-height: 500px): o celular deitado, sem teclado, também cai nessa regra.
2. Acompanhe o teclado com visualViewport
Onde o layout viewport não encolhe (iPhone e Android no padrão), leia a altura do teclado e suba o que precisa ficar visível:
const vv = window.visualViewport;
function teclado() {
const altura = Math.max(0, window.innerHeight - vv.height - vv.offsetTop);
document.documentElement.style.setProperty('--teclado', `${altura}px`);
}
vv.addEventListener('resize', teclado);
vv.addEventListener('scroll', teclado);
teclado();
.caixa-de-mensagem {
position: fixed;
left: 0;
right: 0;
bottom: var(--teclado, 0px);
}
No Android com resizes-content, innerHeight já encolheu junto e a conta dá zero, então o mesmo código serve para os dois sistemas.
3. Deixe a página ter para onde rolar
Num formulário comum, não prenda a altura do body em 100vh com overflow: hidden. Com rolagem, o próprio navegador leva o campo focado para cima do teclado. Se o campo fica dentro de um contêiner com rolagem própria, chame campo.scrollIntoView({ block: 'center' }) no focus.
O que não resolve
- Trocar
position: fixedporabsolute. O rodapé para de cobrir, mas também para de ficar embaixo da tela. env(keyboard-inset-height)e a VirtualKeyboard API. Só funcionam no Chromium e só depois denavigator.virtualKeyboard.overlaysContent = true. O iPhone não tem essa API.100dvhsozinho. No iPhone ele não muda com o teclado. No Android, só comresizes-content.
Como testar
O modo celular do Chrome não abre teclado nenhum, e é por isso que esse defeito chega à produção: no computador a pessoa digita no teclado físico e a tela não muda.
No Breakpane, toque num campo de um celular e o teclado sobe na tela do aparelho com a regra de cada sistema: no iPhone, a página fica igual e só a área visível encolhe; no Android, ele segue o interactive-widget da sua meta viewport. Abra um iPhone e um Galaxy lado a lado e você vê na hora se o botão Enviar ficou atrás do teclado ou se a barra de abas subiu em cima do campo. Liga e desliga com ⇧⌘K (Ctrl+Shift+K no Windows).
Dois limites: o caso do iPhone que empurra a tela inteira quando a página não rola não é reproduzido, e a VirtualKeyboard API fica com valor zero. Se o seu layout depende de um deles, confira uma vez no aparelho. Para o que acontece com a barra do navegador ao rolar, veja 100vh no Safari do iPhone.
Checklist
- Meta viewport com o
interactive-widgetescolhido de propósito. - Com o teclado aberto, a barra de abas e os botões flutuantes saem do caminho.
- O campo e o botão Enviar visíveis com o teclado aberto, num iPhone e num Android, em pé e deitado.
- A página tem para onde rolar, ou o campo chama
scrollIntoViewnofocus.


