Campo com sugestões em HTML: datalist que ajuda sem virar cadastro bagunçado
Use datalist para sugerir valores sem prender a pessoa, carregue opções do servidor sob demanda e normalize no backend para "São Paulo" e "Sao Paulo" não virarem duas cidades.
O relatório de vendas por cidade mostra "São Paulo", "Sao Paulo", "SAO PAULO" e "São Paulo " com espaço no fim. Quatro linhas para a mesma cidade, porque o campo era texto livre. Trocar por um select resolveria a grafia, mas trava quem mora numa cidade que não está na lista. A resposta curta: use <input list> com <datalist> para sugerir, e normalize no servidor para decidir o que é igual. A lista no HTML é conforto. A regra mora no backend.
O exemplo deste guia é o cadastro de uma transportadora com dois campos: cidade de entrega, que precisa bater com uma tabela, e cargo do contato, que aceita qualquer texto mas deve sair padronizado.
O que o datalist faz e o que ele não faz
O datalist liga um campo de texto comum a uma lista de opções. Ao focar ou digitar, o navegador mostra as opções que combinam com o que já foi escrito. A pessoa pode escolher uma delas ou continuar digitando outra coisa. O que vai para o servidor é sempre o texto que ficou no campo.
Três consequências práticas:
- Não existe id escondido. Diferente do
select, não há separação entre o que aparece e o que é enviado. Se você precisa do id da cidade, o servidor tem de encontrar esse id a partir do texto. - O filtro e a aparência variam por navegador. Cada um decide como compara o texto digitado com as opções e como desenha a lista. Não conte que digitar "Sao" vai sugerir "São Paulo" em todos; teste nos navegadores que o seu público usa, principalmente no celular.
- Nada impede um valor fora da lista. O HTML não valida que o texto pertence às opções. Isso é intencional e é a vantagem do componente.
Quando usar datalist, select ou busca no servidor
Use select quando o valor é fechado e curto: estado, tipo de documento, status. Use datalist quando a maioria escolhe de uma lista conhecida, mas um valor novo é legítimo: cargo, marca, tag. Para catálogo grande e fechado, como os mais de cinco mil municípios do país, carregue sugestões do servidor conforme a pessoa digita e valide contra a tabela no envio.
Como montar o campo passo a passo
- Crie o rótulo e o campo. O
labelcontinua obrigatório; a lista não substitui o nome do campo. - Ligue o campo à lista pelo atributo
list, que recebe oiddo datalist. - Preencha as opções com os valores mais comuns, já na grafia oficial.
- Para listas grandes, busque no servidor a partir de duas letras, com um pequeno atraso entre teclas.
- No envio, normalize e decida: encontrou na tabela, grava o id; não encontrou, recusa (catálogo fechado) ou grava o texto limpo (catálogo aberto).
O HTML do cargo, com lista fixa
<label for="cargo">Cargo do contato</label>
<input id="cargo" name="cargo" list="cargos" maxlength="80">
<datalist id="cargos">
<option value="Gerente de logística"></option>
<option value="Comprador"></option>
<option value="Analista fiscal"></option>
<option value="Proprietário"></option>
</datalist>
A cidade, com sugestões vindas do servidor
<label for="cidade">Cidade de entrega</label>
<input id="cidade" name="cidade" list="cidades" autocomplete="address-level2" required>
<datalist id="cidades"></datalist>
<script>
const campo = document.getElementById('cidade');
const lista = document.getElementById('cidades');
let espera;
campo.addEventListener('input', () => {
clearTimeout(espera);
const termo = campo.value.trim();
if (termo.length < 2) return;
espera = setTimeout(async () => {
const resp = await fetch('/api/cidades?q=' + encodeURIComponent(termo));
if (!resp.ok) return;
const nomes = await resp.json(); // ["São Paulo - SP", "São Pedro - SP", ...]
lista.replaceChildren(...nomes.map((nome) => {
const opcao = document.createElement('option');
opcao.value = nome;
return opcao;
}));
}, 250);
});
</script>
O autocomplete="address-level2" diz ao navegador que o campo é cidade, então o preenchimento automático de endereço também ajuda. O post sobre autocomplete certo explica esses valores. Note que o texto da opção inclui a UF: "São José - SC" e "São José - SP" são lugares diferentes, e a sugestão precisa deixar isso claro.
A decisão no servidor: normalizar e conferir
O texto chega como a pessoa deixou: com espaço duplo, sem acento, em caixa alta. A primeira etapa é limpar o que nunca tem significado. A segunda é comparar com a tabela de um jeito que ignore o que não deveria diferenciar.
<?php
$entrada = $_POST['cidade'] ?? '';
$entrada = trim(preg_replace('/\s+/u', ' ', $entrada));
[$nome, $uf] = array_pad(array_map('trim', explode(' - ', $entrada, 2)), 2, '');
$stmt = $pdo->prepare('SELECT id, nome, uf FROM cidades WHERE nome = ? AND uf = ?');
$stmt->execute([$nome, strtoupper($uf)]);
$cidade = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$cidade) {
http_response_code(422);
echo json_encode(['campo' => 'cidade', 'erro' => 'Escolha uma cidade da lista, no formato Cidade - UF.']);
exit;
}
// grava $cidade['id'] no pedido, nunca o texto digitado
A comparação sem acento e sem diferença de maiúsculas fica por conta da collation da coluna. No MySQL 8, uma coluna utf8mb4 com collation utf8mb4_0900_ai_ci (o "ai" é insensível a acento, o "ci" a caixa) faz "sao paulo" bater com "São Paulo". Se a sua tabela usa outra collation, confira antes de confiar nisso; o post sobre acentos que quebram no banco ajuda a diagnosticar.
Para o cargo, que é catálogo aberto, o servidor faz só a limpeza e grava o texto. Se quiser reduzir variações com o tempo, rode uma consulta periódica agrupando cargos parecidos e acrescente os mais frequentes ao datalist.
Acessibilidade e celular
O campo com datalist é anunciado por leitores de tela como um campo com sugestões, mas o comportamento depende da combinação de navegador e leitor. Garanta o básico: rótulo visível, mensagem de erro ligada ao campo e possibilidade de completar o formulário só digitando. Não coloque instruções apenas nas opções. Se o formato exigido é "Cidade - UF", escreva isso perto do campo.
No celular, a lista pode aparecer como barra acima do teclado ou nem aparecer. Por isso o servidor precisa aceitar a entrada digitada à mão e responder com um erro claro quando não achar o valor. O guia formulário que não cansa trata da posição dessa mensagem.
Erros comuns
- Tratar o datalist como validação. Qualquer texto passa, e o banco enche de variações do mesmo valor.
- Gravar o texto quando a regra pede um id. O relatório agrupa errado e a cidade renomeada não atualiza os pedidos antigos.
- Colocar milhares de opções no HTML. A página pesa, a lista fica impossível de navegar e alguns navegadores travam no celular.
- Opções ambíguas. "São José" sem UF faz a pessoa escolher a cidade errada com toda a confiança.
- Usar datalist para valor fechado e curto. Um select resolveria com menos código e sem erro de digitação.
- Esquecer o label. Sem nome acessível, o leitor de tela anuncia só "campo de edição".
Checklist
- Cada campo foi classificado como catálogo fechado ou texto com ajuda.
- Campos fechados validam no servidor e gravam o id, não o texto.
- O servidor remove espaços extras antes de comparar ou gravar.
- A collation da coluna de busca ignora acento e caixa, ou a normalização faz isso no código.
- Listas grandes vêm do servidor sob demanda, com atraso entre teclas.
- As opções incluem o que desfaz ambiguidade, como a UF.
- O formulário foi testado no celular com a lista desligada, digitando tudo à mão.
Rode um SELECT cidade, COUNT(*) FROM pedidos GROUP BY cidade no seu banco. Quantas grafias diferentes aparecem para a mesma cidade, e esse campo deveria ser catálogo fechado ou texto com ajuda?
Referência: http://www.w3schools.com/tags/att_input_list.asp