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.

Atualizado em 5 min de leitura

FormuláriosGalaxy A06 · 360 × 800
Galaxy A06 com um app aberto e as cotas do viewport

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, o 100vh, o 100dvh, o innerHeight e 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: 0 fica 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: fixed por absolute. 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 de navigator.virtualKeyboard.overlaysContent = true. O iPhone não tem essa API.
  • 100dvh sozinho. No iPhone ele não muda com o teclado. No Android, só com resizes-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-widget escolhido 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 scrollIntoView no focus.

Outros guias

  1. FormuláriosiPhone 16 · 393 × 852
    iPhone 16 com um app aberto e a cota de largura de 393 pontos
    iPhone dá zoom ao tocar no campo? Como tirar o zoom do input no Safari
  2. Safe areaiPhone 17 Pro · 402 × 874
    iPhone 17 Pro com um app em tela cheia e as cotas das áreas seguras
    Safe area não funciona? Por que env(safe-area-inset-bottom) dá 0
  3. SafariiPhone 17 Pro · 402 × 874
    iPhone 17 Pro no Safari com as cotas das barras de cima e de baixo
    100vh no Safari do iPhone: a barra de endereço, dvh, svh e lvh