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.
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:
<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.
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:
Guarde três coisas dessa saída, porque elas explicam quase tudo que vem depois:
- 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. - O
typedecide esse formato. A data virou2026-08-22, o horário virou14:30, a semana virou2026-W34. É a especificação mandando, não o idioma do computador de quem preencheu. - 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 |
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:
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);
}
}- Aceitam qualquer coisa:
text,search,tel,url,email,passwordehidden. O navegador guarda o texto sem discutir. - Sanitizam para vazio:
numbere a família inteira de data e hora. Se o conteúdo não bate com o formato, ovaluevira string vazia. Não viranull, não vira erro: vira vazio. - Nunca ficam vazios:
rangejá nasce valendo o meio da faixa ecolorjá nasce#000000. Não existe “não respondeu” nesses dois. fileé o único que reage: ele nem deixa atribuir. LançaInvalidStateError, 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:
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:
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
}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.
<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:
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());
}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.
Já 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.
<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:
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:
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:
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.
<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:
Quatro leituras importantes:
extrasapareceu duas vezes. Marcar duas caixas do mesmonamemanda dois pares com o mesmo nome. No Node,new URLSearchParams(corpo).get('extras')devolve só"banho"; quem devolve as duas égetAll('extras').tosanão apareceu. Caixa desmarcada não virafalse— ela simplesmente não existe no envio. No servidor, “não marcou” é a ausência da chave, e quem trata isso comif (corpo.tosa === false)nunca entra noif.lembretevirou"on", porque essa caixa não temvalue. Funciona, e é ruim: escrevavalue="sim"e o dado passa a se explicar sozinho.- O
radiomandou um valor só. É onamecompartilhado que faz um desmarcar o outro. Trêsradiocomnamediferente 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:
<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">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:
const data = document.getElementById('data');
const hora = document.getElementById('hora');
console.log(data.value, data.valueAsNumber, data.valueAsDate);
console.log(hora.value, hora.valueAsNumber);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.
<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:
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);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 comJSON.stringify.) A especificação manda esconder o caminho real do disco por privacidade, e escolheu esse prefixo fixo. Evaluesó mostra o primeiro arquivo. Nunca usevalueaqui: usefiles.filesé uma lista mesmo com um arquivo só. Semmultiple, ela tem no máximo um item, e sem nada escolhido tem zero. Commultiple, onamevai repetido no envio, como noscheckbox.anotacoes.txtentrou, apesar doaccept="image/*,.pdf". Oacceptfiltra 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.
<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:
Três conclusões que valem uma tarde de depuração:
datalistsugere, não obriga.Lhasa apsofoi enviado sem reclamação — oinputcontinua sendo um campo de texto comum, com uma lista de atalhos. Se o conjunto de respostas é fechado, o controle éselect.selectsemselectedjá 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 faztexto.split('\n')no servidor termina com um\rinvisí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.
<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.
<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:
// `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));
}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.
Perguntas frequentes
Preciso decorar os 22 valores de type?
Posso confiar no type para garantir o formato do dado no servidor?
Por que o campo de moeda não deveria ser type=number?
O que acontece se dois radio tiverem name diferente?
Dúvidas e comentários
Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.
Entrar para perguntarÉ o mesmo login gratuito dos cursos.
Nenhuma dúvida por aqui ainda — a primeira pode ser a sua.
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
- MDN — O elemento <input> — developer.mozilla.org
- HTML Standard — States of the type attribute — html.spec.whatwg.org
- HTML Standard — Form submission algorithm — html.spec.whatwg.org



