Campo de data que aceita teclado e grava a data certa no servidor
Deixe a pessoa digitar ou escolher no calendário, valide os dois caminhos e grave a data em ISO (ano-mês-dia), sem o bug do fuso que muda o dia.
A recepcionista da Clínica Aurora sabe de cor a data de nascimento do paciente: 14/09/1958. O campo do cadastro só abre um calendário, que começa em outubro de 2026. São 68 anos de clique na setinha, ou um seletor de ano escondido que ela não encontrou. No fim, ela desiste e escolhe qualquer data "para corrigir depois".
A resposta curta: o calendário é um atalho, o teclado precisa continuar sendo um caminho completo. Deixe digitar no formato que a pessoa conhece, valide a data de verdade nos dois caminhos e envie ao servidor sempre no formato ISO, 1958-09-14. O texto local é só apresentação.
Por que o seletor vira uma parede
Muitos componentes de calendário tratam o campo de texto como somente leitura: a única forma de preencher é clicar. Para datas próximas, como agendar uma consulta para semana que vem, isso até funciona. Para datas distantes, como nascimento, emissão de documento ou vencimento de contrato, vira uma maratona de cliques. Para quem navega só por teclado ou usa leitor de tela, um calendário mal implementado simplesmente não tem saída.
O segundo problema é o formato. "01/03/2026" é 1º de março para quem lê no Brasil e 3 de janeiro para quem lê nos Estados Unidos. Se o servidor recebe esse texto e o interpreta com a regra errada, o dia e o mês trocam de lugar sem nenhum erro aparente. Por isso o valor que viaja e é gravado precisa ser inequívoco: ano, mês e dia, nessa ordem.
O terceiro é o fuso. Em JavaScript, new Date("2026-03-01") é interpretado como meia-noite em UTC. Exibido no horário de Brasília (UTC-3), isso vira 28 de fevereiro às 21h. Foi assim que a Clínica Aurora passou a ver aniversários um dia antes do certo.
Como montar o campo do jeito certo
- Comece pelo campo nativo quando servir. O
<input type="date">dos navegadores atuais aceita digitação nos segmentos de dia, mês e ano, mostra o formato do idioma do sistema e sempre entrega o valor emAAAA-MM-DD. - Se precisar de componente próprio, mantenha o texto editável. Um campo de texto com dica de formato, mais um botão ao lado que abre o calendário.
- Converta o texto digitado para ISO no navegador e guarde em um campo oculto ou envie direto no corpo da requisição.
- Valide se a data existe: 31/02 e 29/02/2025 não existem.
- Valide de novo no servidor, porque a validação do navegador pode ser contornada.
- Grave em coluna do tipo data, nunca em texto.
O campo nativo, com limites
<label for="nascimento">Data de nascimento</label>
<input type="date" id="nascimento" name="nascimento"
min="1900-01-01" max="2026-10-17"
autocomplete="bday" required>
Os atributos min e max também usam ISO. O autocomplete="bday" permite que o navegador sugira a data de nascimento salva. A aparência do calendário nativo varia entre navegadores e sistemas, e é por isso que muitos projetos trocam por um componente; antes de trocar, pergunte se a aparência vale a perda de acessibilidade que vem de graça com o nativo.
Componente próprio: texto digitável mais conversão
Quando o projeto exige um seletor visual específico, a estrutura mínima é um texto que aceita teclado e uma função que converte e valida.
<label for="nascimento-texto">Data de nascimento (dd/mm/aaaa)</label>
<input type="text" id="nascimento-texto" inputmode="numeric"
placeholder="dd/mm/aaaa" aria-describedby="nascimento-erro">
<button type="button" aria-label="Abrir calendário">Calendário</button>
<input type="hidden" name="nascimento" id="nascimento-iso">
<p id="nascimento-erro" role="alert"></p>
function lerDataBr(texto) {
const m = /^(\d{2})\/(\d{2})\/(\d{4})$/.exec(texto.trim());
if (!m) return null;
const dia = Number(m[1]);
const mes = Number(m[2]);
const ano = Number(m[3]);
const d = new Date(Date.UTC(ano, mes - 1, dia));
if (d.getUTCFullYear() !== ano || d.getUTCMonth() !== mes - 1 || d.getUTCDate() !== dia) {
return null;
}
return m[3] + '-' + m[2] + '-' + m[1];
}
const campo = document.getElementById('nascimento-texto');
const oculto = document.getElementById('nascimento-iso');
const erro = document.getElementById('nascimento-erro');
campo.addEventListener('blur', function () {
const iso = lerDataBr(campo.value);
oculto.value = iso || '';
erro.textContent = iso || campo.value === '' ? '' : 'Data inválida. Use dd/mm/aaaa, por exemplo 14/09/1958.';
});
O truque da validação está no Date.UTC: se você pede 31 de fevereiro, o JavaScript "transborda" para 3 de março. Comparar dia, mês e ano de volta revela que a data não existe. Usar UTC na conta evita que o fuso mude o dia no meio do caminho. Quando o calendário é usado, ele escreve no mesmo campo de texto em dd/mm/aaaa e a mesma função faz o resto: um caminho de validação só.
O calendário também precisa de teclado
Se o calendário abre em diálogo, o padrão de referência é o exemplo "Date Picker Dialog" das práticas de autoria do WAI-ARIA: setas movem o dia, Page Up e Page Down trocam o mês, Enter escolhe, Esc fecha e devolve o foco ao campo. O dia focado precisa de contorno visível. Componentes prontos variam muito nisso; teste com o mouse longe da mão antes de adotar um.
Validar e gravar no servidor
O servidor recebe 1958-09-14 e confere de novo. Em PHP:
<?php
function dataIsoValida(string $valor): ?DateTimeImmutable
{
$d = DateTimeImmutable::createFromFormat('!Y-m-d', $valor);
if ($d === false || $d->format('Y-m-d') !== $valor) {
return null;
}
return $d;
}
$nascimento = dataIsoValida($_POST['nascimento'] ?? '');
if ($nascimento === null) {
http_response_code(422);
exit('Data de nascimento inválida.');
}
$stmt = $pdo->prepare('UPDATE pacientes SET nascimento = ? WHERE id = ?');
$stmt->execute([$nascimento->format('Y-m-d'), $pacienteId]);
O ! no formato zera hora e minuto, e a comparação final recusa datas que o PHP "ajeitaria", como 2026-02-31. A coluna é DATE, não VARCHAR: assim o banco ordena, filtra por período e recusa lixo. Por que isso importa está em número, texto e data não são intercambiáveis, e a mesma lógica de conferir no servidor aparece em validação de plugin não basta.
Data de nascimento não tem fuso: é um dia no calendário. Já data e hora de um evento tem, e aí a conversa é outra, tratada em data e fuso no PHP.
Erros comuns
- Campo de texto somente leitura com calendário. Quem usa teclado não preenche, e datas distantes viram uma maratona de cliques.
- Gravar "01/03/2026" como texto. Ordenação errada, filtro por período impossível e dia trocado com mês conforme quem lê.
- Converter com
new Date("AAAA-MM-DD")e exibir no horário local. No Brasil, a data aparece um dia antes. - Validar só no calendário. A data digitada ou enviada direto pela requisição passa sem conferência.
- Aceitar 31/02 porque a biblioteca "corrige". A data gravada não é a que a pessoa digitou, e ninguém é avisado.
- Esconder o seletor de ano. Data de nascimento exige navegar décadas; sem atalho visível, a pessoa chuta.
Checklist
- A data pode ser preenchida só com o teclado, do foco ao envio.
- O formato esperado aparece no rótulo ou na dica, com exemplo.
- O valor enviado ao servidor está em
AAAA-MM-DD. - Datas inexistentes são recusadas no navegador e no servidor.
- A coluna no banco é do tipo data.
- A data exibida é a mesma gravada, sem deslocamento de fuso.
- O calendário tem foco visível e fecha com Esc devolvendo o foco ao campo.
Vá ao formulário com o campo de data mais usado do seu sistema, guarde o mouse e tente preencher 14/09/1958. Quanto tempo levou, e a data gravada no banco é exatamente essa?
Referência: https://www.syncfusion.com/kb/7204/integrate-ej-components-with-laravel-framework