Pular para o conteúdo
Cursos

DevClub

LógicaFront-endBack-endMobile

IA Club

IA na prática
Estudar programaçãoEstudar IA
LiçãoIniciantecódigo testado

Tipos de input HTML: qual campo usar para cada dado

text, email, number, date, file, checkbox, radio e range: o que cada type muda no teclado do celular, na validação e no valor enviado.

Rodolfo Mori17 min de leitura

O atributo type do input não muda só a aparência do campo. Ele muda o teclado que o celular abre, muda o que o navegador aceita guardar, muda a validação automática e muda o formato exato do texto que chega ao servidor. Escolher o type errado é receber o dado certo em formato errado.

A ficha desta lição é a mesma da Clínica Pata Amiga que aparece em formulário em HTML, a lição anterior da trilha: quem agenda é a tutora Ana Prado e a paciente é a Nina. Lá a pergunta era como o form envia; aqui é o que cada type envia.

Cada dado pede a ferramenta certa

Numa caixa de ferramentas, chave de fenda e alicate até conseguem improvisar o trabalho um do outro, mas você perde precisão e segurança. Os tipos de input funcionam assim. Todos coletam algum valor, porém email, date, number e file dizem ao navegador que ferramenta oferecer e quais verificações básicas fazer.

O nome técnico do atributo é type; ele não transforma o campo num banco de dados tipado. Mesmo input type="number" envia texto no formulário, e cada tipo tem regras próprias para value. Pegue a ficha da clínica e, antes de ver o HTML, escolha o tipo de cada dado: e-mail da tutora, data da consulta, peso do pet e foto da carteirinha. Depois compare com o teclado, a validação e o valor real que o navegador produz. A escolha certa começa no dado, não na aparência do campo.

A ficha inteira enviada de uma vez

Antes de discutir um type por vez, vale ver todos juntos. Esta é a ficha da clínica, com um campo para cada type. Cortei metade dela para caber na página; os campos que faltam aqui (tel, url, password, time, select, textarea e companhia) aparecem cada um no seu tópico, mas todos estavam na página que eu enviei:

html
<form action="/agendar" method="post">
  <input type="hidden" name="unidade" value="pinheiros-02">

  <label for="tutor">Nome do tutor</label>
  <input type="text" id="tutor" name="tutor">

  <label for="email">E-mail</label>
  <input type="email" id="email" name="email">

  <label for="peso">Peso (kg)</label>
  <input type="number" id="peso" name="peso" step="0.1" min="0.5" max="90">

  <label for="agitacao">Agitação na sala de espera</label>
  <input type="range" id="agitacao" name="agitacao" min="1" max="5" value="3">

  <label for="data">Data da consulta</label>
  <input type="date" id="data" name="data">

  <label for="coleira">Cor da coleira</label>
  <input type="color" id="coleira" name="coleira">

  <label><input type="checkbox" name="extras" value="banho"> Banho</label>
  <label><input type="checkbox" name="lembrete"> Quero lembrete por SMS</label>

  <label><input type="radio" name="porte" value="pequeno"> Pequeno</label>

  <button type="submit" name="acao" value="agendar">Agendar</button>
</form>

Do outro lado, o mesmo servidor-espelho da lição anterior: ele não interpreta nada, só imprime o corpo cru da requisição e depois os pares já separados.

js
import { createServer } from 'node:http';

createServer(async (req, res) => {
  const pedacos = [];
  for await (const p of req) pedacos.push(p);
  const corpo = Buffer.concat(pedacos).toString('utf8');

  console.log('--- corpo cru ---');
  console.log(corpo);
  console.log('--- pares ---');
  for (const [nome, valor] of new URLSearchParams(corpo)) {
    console.log(`${nome} = ${JSON.stringify(valor)}`);
  }

  res.writeHead(200, { 'content-type': 'text/plain; charset=utf-8' });
  res.end('ok');
}).listen(4530);

Preenchi a ficha inteira num Chrome de verdade e cliquei em Agendar. Isto é o que o servidor recebeu:

--- corpo cru --- unidade=pinheiros-02&tutor=Ana+Prado&raca=Shih+tzu&email=ana.prado%40exemplo.com.br&whatsapp=%2811%29+98888-7766&perfil=https%3A%2F%2Fexemplo.com.br%2Fpets%2Fnina&senha=pataAmiga%232026&peso=7.4&agitacao=4&data=2026-08-22&hora=14%3A30&retorno=2026-09-05T09%3A00&plano=2026-08&campanha=2026-W34&coleira=%231f7a3d&extras=banho&extras=unha&lembrete=on&porte=pequeno&especie=cao&sintomas=A+Nina+est%C3%A1+co%C3%A7ando+a+orelha+direita+desde+s%C3%A1bado.&acao=agendar --- pares --- unidade = "pinheiros-02" tutor = "Ana Prado" raca = "Shih tzu" email = "ana.prado@exemplo.com.br" whatsapp = "(11) 98888-7766" perfil = "https://exemplo.com.br/pets/nina" senha = "pataAmiga#2026" peso = "7.4" agitacao = "4" data = "2026-08-22" hora = "14:30" retorno = "2026-09-05T09:00" plano = "2026-08" campanha = "2026-W34" coleira = "#1f7a3d" extras = "banho" extras = "unha" lembrete = "on" porte = "pequeno" especie = "cao" sintomas = "A Nina está coçando a orelha direita desde sábado." acao = "agendar"

Guarde três coisas dessa saída, porque elas explicam quase tudo que vem depois:

  1. Tudo é texto. O peso não chegou como número 7.4; chegou como a string "7.4". Não existe tipo no envio de formulário — existe formato.
  2. O type decide esse formato. A data virou 2026-08-22, o horário virou 14:30, a semana virou 2026-W34. É a especificação mandando, não o idioma do computador de quem preencheu.
  3. A senha viajou legível. type="password" esconde na tela e mais nada. Quem protege no caminho é o HTTPS.

Tabela de decisão: o dado que você quer, o type certo

o dado que você quer type o que chega ao servidor
nome, observação curta text o texto como foi digitado
e-mail email o texto, com @ obrigatório
telefone, CPF, CEP tel o texto com parênteses e traços
endereço de site url o texto, com esquema obrigatório
senha password o texto legível
quantidade, peso, idade number um número em formato inglês, ou vazio
nota de 1 a 5, volume range sempre um número, nunca vazio
data date AAAA-MM-DD
horário time HH:MM
data e hora juntas datetime-local AAAA-MM-DDTHH:MM
mês de referência month AAAA-MM
semana week AAAA-Wnn
cor color #rrggbb minúsculo, sempre
sim ou não checkbox o value, ou nada
uma opção entre poucas radio o value da escolhida
arquivo file o conteúdo do arquivo, em blocos
dado técnico invisível hidden o value fixo

Duas armadilhas moram nessa tabela. Telefone, CPF e CEP não são number: eles têm zero à esquerda, parênteses e traços, e number joga tudo isso fora. E dinheiro também não é number, porque o campo aceita notação científica e limpa o valor inteiro quando o conteúdo não vira número — o tópico sobre number mostra isso acontecendo.

As três famílias: quem sanitiza, quem aceita tudo, quem nunca fica vazio

Criei um input de cada type no Chrome, li o value recém-criado e depois tentei enfiar o texto "Nina 7,4" em todos. O resultado separa os tipos em três famílias bem definidas, mais um caso isolado:

js
const TIPOS = ['text', 'search', 'tel', 'url', 'email', 'password', 'date', 'month',
  'week', 'time', 'datetime-local', 'number', 'range', 'color', 'checkbox',
  'radio', 'file', 'hidden'];

for (const tipo of TIPOS) {
  const campo = document.createElement('input');
  campo.type = tipo;
  const inicial = campo.value;
  try {
    campo.value = 'Nina 7,4';
    console.log(tipo, inicial, campo.value);
  } catch (erro) {
    console.log(tipo, inicial, 'lança ' + erro.name);
  }
}
text value inicial="" depois de value="Nina 7,4" -> "Nina 7,4" search value inicial="" depois de value="Nina 7,4" -> "Nina 7,4" tel value inicial="" depois de value="Nina 7,4" -> "Nina 7,4" url value inicial="" depois de value="Nina 7,4" -> "Nina 7,4" email value inicial="" depois de value="Nina 7,4" -> "Nina 7,4" password value inicial="" depois de value="Nina 7,4" -> "Nina 7,4" date value inicial="" depois de value="Nina 7,4" -> "" month value inicial="" depois de value="Nina 7,4" -> "" week value inicial="" depois de value="Nina 7,4" -> "" time value inicial="" depois de value="Nina 7,4" -> "" datetime-local value inicial="" depois de value="Nina 7,4" -> "" number value inicial="" depois de value="Nina 7,4" -> "" range value inicial="50" depois de value="Nina 7,4" -> "50" color value inicial="#000000" depois de value="Nina 7,4" -> "#000000" checkbox value inicial="on" depois de value="Nina 7,4" -> "Nina 7,4" radio value inicial="on" depois de value="Nina 7,4" -> "Nina 7,4" file value inicial="" depois de value="Nina 7,4" -> lança InvalidStateError hidden value inicial="" depois de value="Nina 7,4" -> "Nina 7,4"
  • Aceitam qualquer coisa: text, search, tel, url, email, password e hidden. O navegador guarda o texto sem discutir.
  • Sanitizam para vazio: number e a família inteira de data e hora. Se o conteúdo não bate com o formato, o value vira string vazia. Não vira null, não vira erro: vira vazio.
  • Nunca ficam vazios: range já nasce valendo o meio da faixa e color já nasce #000000. Não existe “não respondeu” nesses dois.
  • file é o único que reage: ele nem deixa atribuir. Lança InvalidStateError, porque só quem está no computador escolhe arquivo.

Repare também no value inicial="on" do checkbox e do radio: sem atributo value, o valor padrão deles é literalmente a string on.

Essas três famílias reaparecem na rede. Abri a mesma ficha da clínica e cliquei em Agendar sem preencher nada:

unidade=pinheiros-02&tutor=&raca=&email=&whatsapp=&perfil=&senha=&peso=&agitacao=3&data=&hora=&retorno=&plano=&campanha=&coleira=%23000000&especie=&sintomas=&acao=agendar

Quase tudo chegou como string vazia. Mas agitacao=3 e coleira=%23000000 chegaram preenchidos — range e color mandam o padrão mesmo sem ninguém encostar neles, e %23 é o # codificado. E sumiu da lista quem não estava marcado: nenhum extras, nenhum lembrete, nenhum porte. Campo de texto vazio existe valendo ""; caixa desmarcada não existe.

Dá para conferir isso lendo a propriedade type do elemento, que é o que o navegador entendeu, e não o que você escreveu:

js
for (const escrito of ['datetime', 'telefone', null, 'TEL']) {
  const campo = document.createElement('input');
  if (escrito !== null) campo.setAttribute('type', escrito);

  console.log(campo.getAttribute('type'), campo.type);
  //          o que está no HTML          o que o navegador entendeu
}
escrito="datetime" campo.type="text" escrito="telefone" campo.type="text" escrito=null campo.type="text" escrito="TEL" campo.type="tel"

Sem atributo nenhum também dá text — esse é o padrão. E TEL em maiúsculas funciona: o valor não diferencia maiúscula de minúscula.

text, search, tel, url e email: o teclado muda, a garantia não

Esses cinco guardam texto do mesmo jeito. O que muda entre eles é o teclado que o celular abre, a sugestão de preenchimento e o quanto o navegador cobra na hora do envio.

html
<label for="tutor">Nome do tutor</label>
<input type="text" id="tutor" name="tutor" autocomplete="name">

<label for="raca">Raça</label>
<input type="search" id="raca" name="raca">

<label for="email">E-mail</label>
<input type="email" id="email" name="email" autocomplete="email">

<label for="whatsapp">WhatsApp</label>
<input type="tel" id="whatsapp" name="whatsapp" autocomplete="tel">

<label for="perfil">Perfil do pet</label>
<input type="url" id="perfil" name="perfil">

No celular, tel abre o teclado numérico do telefone e email acrescenta o @ à barra de espaço. Quem escolhe o teclado é o sistema operacional, a partir do type; o HTML só declara que tipo de dado é aquele. Só esse detalhe já corta boa parte dos erros de digitação em formulário mobile — e é de graça.

Agora a parte que engana. Coloquei três valores em cada campo e perguntei se o navegador aceitaria enviar:

js
const fone = document.getElementById('whatsapp');
const perfil = document.getElementById('perfil');

for (const v of ['(11) 98888-7766', 'não tenho', '@ninashihtzu']) {
  fone.value = v;
  console.log('tel', v, fone.checkValidity());
}

for (const v of ['exemplo.com.br/pets/nina', 'https://exemplo.com.br/pets/nina', 'zap://oi']) {
  perfil.value = v;
  console.log('url', v, perfil.checkValidity());
}
tel "(11) 98888-7766" válido=true tel "não tenho" válido=true tel "@ninashihtzu" válido=true url "exemplo.com.br/pets/nina" válido=false url "https://exemplo.com.br/pets/nina" válido=true url "zap://oi" válido=true

type="tel" não valida nada. Isso é proposital: número de telefone tem formato diferente em cada país, e a especificação preferiu não escolher um. Se você precisa de um formato, é pattern — assunto da validação de formulário sem JavaScript.

type="url" exige um endereço absoluto, com esquema. Note que zap://oi passou: ele cobra que exista um esquema, não que seja http. Um endereço sem https:// na frente, que é como as pessoas escrevem, é recusado — por isso o placeholder desse campo tem que mostrar o https://.

O email fica no meio do caminho: cobra um @ com alguma coisa dos dois lados, e nada além disso. ana@exemplo, sem ponto nenhum, é um e-mail válido para o navegador. E o search é um text com dois enfeites — um “x” para limpar e a promessa de aparecer no histórico de buscas do campo; nada disso muda o envio.

number e range: o valor chega como texto — quando chega

O number é o campo que mais dá problema, e por um motivo específico: quando o conteúdo não vira número, ele apaga o campo inteiro em vez de reclamar.

html
<label for="peso">Peso (kg)</label>
<input type="number" id="peso" name="peso" step="0.1" min="0.5" max="90">

Digitei sete quilos nesse campo, tecla por tecla, como uma tutora apressada faria:

peso.value = "" peso.valueAsNumber = NaN peso.validity.badInput = true peso.validationMessage = "Please enter a number."

O campo mostra sete quilos na tela, mas o value é string vazia. Se você lê esse campo com JavaScript sem checar nada, conclui que a pessoa não preencheu — e é exatamente esse o bug que chega como “o formulário some com o peso do animal”. Quem separa “não digitou” de “digitou coisa que não vira número” é validity.badInput. A mensagem sai no idioma da interface do navegador.

Quando o conteúdo é aceito, o number normaliza sozinho. Limpei o campo e digitei 7,4kg, com vírgula e com unidade, num Chrome em português:

peso.value = "7.4" typeof string peso.valueAsNumber = 7.4 typeof number

A vírgula virou ponto e o kg sumiu. E olhe o typeof: value é string mesmo em type="number". Quem devolve número de verdade é valueAsNumber. Ela existe em todo input, mas só responde nos tipos numéricos e de data: num type="text" valendo "7" ela devolve NaN, não 7. Sem ela você acaba concatenando em vez de somar — o clássico "7.4" + 1 === "7.41", que a lição de converter string em número destrincha.

min e max não impedem a digitação; eles marcam o campo como inválido:

peso.value = "120" peso.checkValidity() = false peso.validity.rangeOverflow = true

O range é o mesmo motor com outra cara: um controle deslizante. Ele serve para grandeza aproximada, em que o número exato não importa — a agitação da Nina na sala de espera, de 1 a 5. Como ele nunca fica vazio, nunca use range para uma pergunta que aceite “não sei”: o servidor vai receber o meio da faixa e achar que foi resposta.

checkbox e radio: mesmo name, values diferentes

Essa dupla é a que mais confunde, porque o name funciona ao contrário do resto do formulário. Nos outros campos, name é o nome do dado. Aqui, name é o nome do grupo.

html
<fieldset>
  <legend>Serviços extras</legend>
  <label><input type="checkbox" name="extras" value="banho"> Banho</label>
  <label><input type="checkbox" name="extras" value="tosa"> Tosa</label>
  <label><input type="checkbox" name="extras" value="unha"> Corte de unha</label>
  <label><input type="checkbox" name="lembrete"> Quero lembrete por SMS</label>
</fieldset>

<fieldset>
  <legend>Porte</legend>
  <label><input type="radio" name="porte" value="pequeno"> Pequeno</label>
  <label><input type="radio" name="porte" value="medio"> Médio</label>
  <label><input type="radio" name="porte" value="grande"> Grande</label>
</fieldset>

Na ficha do começo eu marquei banho, corte de unha, o lembrete e o porte pequeno. Estas são as linhas da saída que interessam aqui:

extras = "banho" extras = "unha" lembrete = "on" porte = "pequeno"

Quatro leituras importantes:

  • extras apareceu duas vezes. Marcar duas caixas do mesmo name manda dois pares com o mesmo nome. No Node, new URLSearchParams(corpo).get('extras') devolve só "banho"; quem devolve as duas é getAll('extras').
  • tosa não apareceu. Caixa desmarcada não vira false — ela simplesmente não existe no envio. No servidor, “não marcou” é a ausência da chave, e quem trata isso com if (corpo.tosa === false) nunca entra no if.
  • lembrete virou "on", porque essa caixa não tem value. Funciona, e é ruim: escreva value="sim" e o dado passa a se explicar sozinho.
  • O radio mandou um valor só. É o name compartilhado que faz um desmarcar o outro. Três radio com name diferente viram três botões que ligam e nunca desligam.

date, time, month e week: o formato que o navegador envia

A tela mostra a data no formato do computador de quem preenche — no Brasil, 22/08/2026. O que sai pela rede é sempre o formato ISO, independente disso:

html
<label for="data">Data da consulta</label>
<input type="date" id="data" name="data" min="2026-08-22">

<label for="hora">Horário</label>
<input type="time" id="hora" name="hora" min="08:00" max="18:00" step="1800">

<label for="retorno">Retorno</label>
<input type="datetime-local" id="retorno" name="retorno">
data = "2026-08-22" hora = "14:30" retorno = "2026-09-05T09:00" plano = "2026-08" campanha = "2026-W34"

Esse desencontro entre o que aparece e o que é enviado é a maior vantagem do type="date": você recebe uma data em formato universal sem ter que ensinar ninguém a digitar, e sem descobrir tarde demais que 03/04 era 3 de abril para o tutor e 4 de março para o sistema. Os atributos min, max e step funcionam como nos números — o step="1800" acima trava o horário em blocos de 30 minutos, porque a unidade do time é o segundo.

Do lado do JavaScript, esses campos também têm as versões tipadas:

js
const data = document.getElementById('data');
const hora = document.getElementById('hora');

console.log(data.value, data.valueAsNumber, data.valueAsDate);
console.log(hora.value, hora.valueAsNumber);
data.value = "2026-08-22" data.valueAsNumber = 1787356800000 data.valueAsDate = 2026-08-22T00:00:00.000Z hora.value = "14:30" hora.valueAsNumber = 52200000

valueAsDate devolve um Date em UTC — meia-noite do dia escolhido, no fuso zero. É a mesma armadilha de new Date('2026-08-22').getDate(), que no Brasil responde 21, porque UTC menos três horas volta um dia. Já valueAsNumber do time é o número de milissegundos desde a meia-noite: 52.200.000 são as 14h30.

month e week são os primos pouco usados. 2026-08 é direto; 2026-W34 é a semana 34 no padrão ISO, que começa na segunda-feira e não bate com o calendário comercial brasileiro. Só use week se o seu sistema já pensa em semana ISO.

file: o navegador te dá uma lista, não um caminho

O campo de arquivo é o único que não guarda um texto. Ele guarda uma lista de objetos File, e o value que você lê é uma cortina de fumaça.

html
<form action="/agendar" method="post" enctype="multipart/form-data">
  <label for="exames">Exames e carteira de vacinação</label>
  <input type="file" id="exames" name="exames" accept="image/*,.pdf" multiple>
  <button type="submit">Enviar</button>
</form>

Escolhi três arquivos de uma vez e li o que o elemento tinha dentro:

js
const exames = document.getElementById('exames');

console.log(exames.value, exames.files.length);
for (const f of exames.files) console.log(f.name, f.size, f.type);
exames.value = "C:\\fakepath\\hemograma-nina.pdf" exames.files.length = 3 exames.files[0] = hemograma-nina.pdf 18 bytes "application/pdf" exames.files[1] = carteira-nina.png 29 bytes "image/png" exames.files[2] = anotacoes.txt 21 bytes "text/plain"

Três coisas nessa saída assustam na primeira vez:

  • C:\fakepath\, num macOS, sem Windows por perto. (As barras aparecem dobradas na saída porque eu imprimi com JSON.stringify.) A especificação manda esconder o caminho real do disco por privacidade, e escolheu esse prefixo fixo. E value só mostra o primeiro arquivo. Nunca use value aqui: use files.
  • files é uma lista mesmo com um arquivo só. Sem multiple, ela tem no máximo um item, e sem nada escolhido tem zero. Com multiple, o name vai repetido no envio, como nos checkbox.
  • anotacoes.txt entrou, apesar do accept="image/*,.pdf". O accept filtra o que aparece na janela de seleção — é conveniência, não barreira. Quem escolhe pode trocar o filtro para “todos os arquivos”, e um cliente sem navegador nem vê o atributo.

E o campo de arquivo é o único type que obriga a mexer no form em volta: sem enctype="multipart/form-data" e method="post", só o nome do arquivo viaja. Esse mecanismo está detalhado na lição de formulário em HTML.

select, textarea e datalist: quando o input não serve

Nem todo campo é input. Três controles têm tag própria, e a diferença entre eles é quem manda no conjunto de respostas.

html
<label for="especie">Espécie</label>
<select id="especie" name="especie">
  <option value="cao">Cão</option>
  <option value="gato">Gato</option>
</select>

<label for="alergias">Alergias</label>
<select id="alergias" name="alergias" multiple>
  <option value="frango">Frango</option>
  <option value="milho">Milho</option>
  <option value="corante">Corante</option>
</select>

<label for="raca">Raça</label>
<input type="text" id="raca" name="raca" list="racas">
<datalist id="racas">
  <option value="Shih tzu"></option>
  <option value="Poodle"></option>
  <option value="Vira-lata caramelo"></option>
</datalist>

<label for="sintomas">Sintomas</label>
<textarea id="sintomas" name="sintomas"></textarea>

Digitei Lhasa apso na raça (que não está na lista de sugestões), marquei duas alergias, não toquei na espécie e escrevi duas linhas nos sintomas:

raca = "Lhasa apso" especie = "cao" alergias = "frango" alergias = "corante" sintomas = "A Nina coça a orelha direita.\r\nPerdeu pelo no pescoço."

Três conclusões que valem uma tarde de depuração:

  • datalist sugere, não obriga. Lhasa apso foi enviado sem reclamação — o input continua sendo um campo de texto comum, com uma lista de atalhos. Se o conjunto de respostas é fechado, o controle é select.
  • select sem selected já vem respondido. Eu não encostei na espécie e chegou "cao", a primeira opção. Para existir o estado “não escolheu”, ponha uma primeira opção vazia: <option value="">Selecione</option>.
  • A quebra de linha do textarea é \r\n. O navegador normaliza para CRLF antes de enviar. Quem faz texto.split('\n') no servidor termina com um \r invisível no fim de cada linha, e passa a tarde procurando um espaço que não existe.

hidden, color, password e os botões

Sobram os tipos de canto, que resolvem um problema cada.

html
<input type="hidden" name="unidade" value="pinheiros-02">
<input type="color" name="coleira" value="#1f7a3d">
<input type="password" name="senha" autocomplete="current-password">

<button type="submit" name="acao" value="agendar">Agendar</button>
<button type="reset">Limpar</button>
<button type="button" id="calcular">Calcular idade humana</button>

hidden carrega um dado técnico que ninguém precisa ver — qual unidade da clínica está atendendo, por exemplo. Ele não é secreto: aparece no código fonte da página e pode ser trocado no DevTools em cinco segundos. Nunca ponha ali preço, nível de permissão ou id de usuário sem conferir de novo no servidor.

color sempre entrega #rrggbb em minúsculas e nunca vem vazio: sem value, começa em #000000, o que torna “preto” e “não respondi” indistinguíveis. Se a cor for opcional, some um checkbox “usar cor personalizada” ao lado.

E o type do botão é o detalhe que mais surpreende: dentro de um form, um <button> sem type é submit. O botão “Calcular idade humana” acima, sem o type="button", enviaria a ficha a cada clique. Repare também que o botão de envio pode ter name e value — foi assim que acao = "agendar" apareceu na saída lá do começo, e é como você distingue “Salvar rascunho” de “Enviar” num formulário com dois botões.

placeholder não é label (e nunca foi)

Todo campo desta lição apareceu com um label. Vale conferir o que acontece quando ele falta — e para isso dá para perguntar ao Chrome qual nome acessível cada campo recebeu, e de onde esse nome veio. É o nome que o leitor de tela anuncia e que o comando de voz procura.

html
<input type="text" name="tutor" placeholder="Nome do tutor">

<label for="pet">Nome do pet</label>
<input type="text" id="pet" name="pet" placeholder="Nina">

<label for="fone">Telefone</label>
<input type="tel" id="fone" name="fone" aria-label="WhatsApp do tutor">

<input type="email" name="email" title="E-mail para o lembrete">

<input type="search" name="busca">

O Chrome mantém, ao lado do DOM, uma segunda árvore — a árvore de acessibilidade — e dá para pedir ela inteira pelo protocolo de depuração. Cada campo vem com a lista de candidatos a nome e a marca de qual foi descartado:

js
// `pagina` é a página do Playwright já aberta com o formulário acima
const cdp = await pagina.context().newCDPSession(pagina);
await cdp.send('Accessibility.enable');
const { nodes } = await cdp.send('Accessibility.getFullAXTree');

const nomeDaFonte = (s) => (s.nativeSource === 'labelfor' ? 'label' : s.attribute ?? s.type);

for (const no of nodes) {
  const papel = no.role?.value;
  if (papel !== 'textbox' && papel !== 'searchbox') continue;

  const fontes = (no.name?.sources ?? []).filter((s) => s.value);
  const venceu = fontes.find((s) => !s.superseded);
  const perdeu = fontes.filter((s) => s.superseded && s.value.value !== venceu?.value.value);

  console.log(papel, no.name.value, venceu && nomeDaFonte(venceu), perdeu.map(nomeDaFonte));
}
textbox nome="Nome do tutor" venceu: placeholder perdeu: — textbox nome="Nome do pet" venceu: label perdeu: placeholder textbox nome="WhatsApp do tutor" venceu: aria-label perdeu: label textbox nome="E-mail para o lembrete" venceu: title perdeu: — searchbox nome="" venceu: — perdeu: —

A saída mostra uma disputa com ordem fixa: aria-label ganha do label, o label ganha do placeholder, e o title só entra quando não há mais nada. O último campo, sem nada, ficou sem nome nenhum — para quem usa leitor de tela, é uma caixa anônima.

Repare no terceiro caso: o campo mostra “Telefone” na tela e se chama “WhatsApp do tutor” para quem escuta. aria-label sobrepõe o texto visível em silêncio, e isso quebra comando de voz — a pessoa fala “Telefone” e nada acontece. Se precisar de um nome diferente, mude o label.

E o primeiro caso é o mais comum de todos. O placeholder sozinho funciona como nome acessível, então a página não fica quebrada. O problema é outro: ele some da tela no instante em que a pessoa começa a digitar, junto com a única pista do que era aquele campo. Quem escreveu o e-mail errado não tem como conferir sem apagar tudo. placeholder é exemplo (ex.: (11) 98888-7766), label é pergunta. O resto do assunto está em acessibilidade em HTML.

O que vem depois

Você já sabe escolher o type e já sabe o formato exato que cada um entrega. O passo seguinte é fazer o navegador recusar o envio quando o dado não serve, com required, pattern e minlength: é a validação de formulário sem JavaScript, a próxima lição da trilha de HTML. Depois dela, ler esses valores na página com JavaScript fica quase trivial, porque a parte difícil — o formato — o type já resolveu. Se quiser o mapa inteiro antes de continuar, o guia de HTML mostra a ordem de estudo.

Prefere aprender em vídeo?

Tem uma aula sobre este assunto no nosso canal.

Ver todos os vídeos do canal
  • html
  • input
  • formulario
  • checkbox
  • select

Perguntas frequentes

Preciso decorar os 22 valores de type?
Não. Na prática você usa oito ou nove: text, email, tel, password, number, date, checkbox, radio e file. Os outros existem para casos específicos, e o navegador cai em text sempre que não reconhece o valor que você escreveu.
Posso confiar no type para garantir o formato do dado no servidor?
Nunca. O type é uma ajuda a quem preenche, não uma barreira. Qualquer pessoa abre o DevTools, troca o type para text e envia o que quiser — e um cliente HTTP sem navegador nenhum nem passa perto dessa checagem. Confira o formato de novo no servidor, sempre.
Por que o campo de moeda não deveria ser type=number?
Porque o number aceita notação científica e sinal de menos, mostra setinhas de incremento e limpa o campo inteiro quando o conteúdo não vira número. Para valor em reais, use type=text com inputmode="decimal" e formate na mão, ou trabalhe em centavos com um inteiro.
O que acontece se dois radio tiverem name diferente?
Eles deixam de ser um grupo. Cada um vira um botão independente que liga e nunca mais desliga, porque só outro radio do mesmo name consegue desmarcá-lo. Radio que não desmarca é quase sempre name errado.

Dúvidas e comentários

Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.

Todo o código deste artigo foi executado em Node 24.16.0, playwright-core 1.62.1 e Chrome 151.0.7922.170 no macOS, e as saídas exibidas são as reais — como produzimos este conteúdo.

Fontes consultadas

  1. MDN — O elemento <input> — developer.mozilla.org
  2. HTML Standard — States of the type attribute — html.spec.whatwg.org
  3. HTML Standard — Form submission algorithm — html.spec.whatwg.org

Continue por aqui