Date picker
Transforma um <input type="text"> num campo de data com calendário, período e hora.
Na tela aparece o formato do idioma da página; no formulário vai ISO, que é o que o Django
espera. A digitação com máscara continua valendo, para quem digita mais rápido do que clica.
Exemplos
O mesmo <input> com atributos diferentes. Embaixo de cada campo aparece o valor ISO,
que é o que chega no servidor.
Data e hora
data-time="true" acrescenta as colunas de hora e minuto, de 5 em 5.
Período com hora
Com hora o painel não fecha sozinho: o botão Aplicar confirma.
Minuto a minuto, com segundos
data-minute-step="1" lista de 00 a 59; data-seconds="true" põe a terceira coluna.
Limites
data-min e data-max em aaaa-mm-dd. Fora deles o dia fica desativado.
Só dias úteis, a partir de hoje
disabledDates recebe uma função e não existe como atributo — vai em JavaScript.
Atalhos de período
data-presets="true" liga Hoje, Últimos 7 dias, Este mês e os outros.
Atalhos próprios
Uma lista de { label, value() }, em JavaScript.
Número da semana
data-week-numbers="true" mostra a semana ISO-8601 à esquerda.
Outro idioma
data-locale="en-US": mês antes do dia, semana começando no domingo, nomes em inglês.
Formato próprio
data-format="dd.MM.yyyy". A máscara acompanha o formato sozinha.
Com ícone
O .tuc-input-group do formulário, com um SVG antes do campo.
Com erro
aria-invalid="true" no campo — o Django 5 já escreve.
A data precisa ser depois de hoje.
Como usar
Marque o <input> e ele inicializa sozinho no carregamento e a cada
htmx:afterSwap. O componente veste a classe .tuc-input no campo, então não precisa escrevê-la.
<input type="text" name="date" data-tuc-datepicker>
<input type="text" name="when" data-tuc-datepicker data-time="true">
<input type="text" name="period" data-tuc-datepicker data-mode="range">
<input type="text" name="window" data-tuc-datepicker data-mode="range" data-time="true">| Atributo | Padrão | Para quê |
|---|---|---|
data-mode | single | range para período |
data-time | false | Acrescenta o seletor de hora |
data-minute-step | 5 | Degrau da coluna de minutos; 1 lista todos |
data-seconds | false | Coluna de segundos |
data-min data-max | — | Limites, em aaaa-mm-dd |
data-months | 1, ou 2 em período | Quantos meses lado a lado |
data-presets | false | true liga os atalhos — só no modo período |
data-week-numbers | false | Coluna com a semana ISO |
data-locale | <html lang> | Nomes, ordem de dia e mês, início da semana e relógio de 12 ou 24 horas |
data-format | do idioma | Formato de exibição, como dd/MM/yyyy |
data-iso-name | o name | name do campo escondido com o ISO |
data-placement | bottom-center | Lado e alinhamento do painel |
data-native | false | true usa o seletor do sistema |
No formulário do Django
class EventForm(forms.ModelForm):
class Meta:
model = Event
fields = ["date", "start", "period"]
widgets = {
"date": forms.TextInput(attrs={"data-tuc-datepicker": ""}),
"start": forms.TextInput(attrs={"data-tuc-datepicker": "", "data-time": "true"}),
"period": forms.TextInput(attrs={"data-tuc-datepicker": "", "data-mode": "range"}),
}Não use type="date" no widget: ele ligaria o calendário do navegador por cima do nosso. O valor inicial pode vir
em ISO ou no formato do idioma — o componente lê os dois.
Em JavaScript
Para o que não cabe em atributo — disabledDates, atalhos próprios, firstDayOfWeek,
autoApply, clearable, onChange — e para ler e escrever o valor.
const dp = new Tucano.DatePicker('#delivery', {
mode: 'range',
time: true,
minuteStep: 15,
min: '2026-01-01',
disabledDates: (d) => d.getDay() === 0, // domingo fechado
onChange: (value, { iso }) => console.log(value, iso),
});
dp.setValue({ start: '2026-03-01', end: '2026-03-15' });
dp.getValue(); // { start: Date, end: Date } — no modo simples, um Date
dp.clear();
dp.open();
dp.destroy();// Atalhos próprios: value() devolve { start, end } e é chamado a cada abertura.
new Tucano.DatePicker('#period', {
mode: 'range',
presets: [
{ label: 'Próximos 7 dias', value: () => {
const today = Tucano.dates.startOfDay(new Date());
return { start: today, end: Tucano.dates.addDays(today, 6) };
} },
],
});Valor e formulário
O campo visível mostra 25/12/2026; um <input type="hidden"> criado ao lado leva o
name e o valor em ISO. O servidor recebe ISO e nunca o texto formatado, que mudaria com o idioma de quem preencheu.
POST date = 2026-12-25
POST start = 2026-12-25T09:30 # DateTimeField lê direto
POST exact = 2026-12-25T09:30:15 # com data-seconds
POST period = 2026-03-01,2026-03-15 # início e fim, separados por vírgulaDateField e DateTimeField fazem o parse disso sem configuração. O período chega num campo só; separe no clean_:
class BookingForm(forms.Form):
period = forms.CharField()
def clean_period(self):
start, _, end = self.cleaned_data["period"].partition(",")
return date.fromisoformat(start), date.fromisoformat(end)Para editar, devolva o valor em ISO no value — o componente lê ISO e também o formato do idioma. Com
data-iso-name o campo visível fica com o próprio name e o escondido ganha outro, e os dois são postados.
Campo sem name e sem data-iso-name não ganha escondido nenhum.
Cada escolha dispara tucano:change no campo visível, e também o change nativo, então validação e
hx-trigger="change" enxergam o valor.
document.querySelector('#due_date').addEventListener('tucano:change', (e) => {
e.detail.value; // Date, ou { start, end } no período
e.detail.iso; // '2026-12-25' — o mesmo texto do campo escondido
e.detail.instance; // o DatePicker
});Aqui e.target.name vem vazio
O evento sai do campo visível, e o name mudou para o escondido. Use e.detail.iso, ou leia o escondido, que fica logo depois do campo.
Digitação
O campo tem máscara tirada do próprio formato de exibição, então ela acompanha o idioma sem configuração. Digitam-se só os números; barra, dois-pontos e o travessão do período entram sozinhos.
| Modo | Máscara em pt-BR |
|---|---|
| Data | dd/mm/aaaa |
| Data e hora | dd/mm/aaaa hh:mm |
| Período | dd/mm/aaaa — dd/mm/aaaa |
| Período com hora | dd/mm/aaaa hh:mm — dd/mm/aaaa hh:mm |
Com a máscara completa o calendário já pula para a data digitada, sem fechar e sem mudar o valor. O valor se confirma ao sair do campo
ou com Enter enquanto o painel está aberto, que também fecha; Esc descarta o que foi digitado. Texto que não vira data volta ao valor anterior, e data fora de
min, max ou disabledDates não é aceita — nem puxada para o limite. Período sem um fim válido é recusado inteiro. Apagar em cima de um separador remove o dígito
vizinho, em vez de travar.
Formato com nome de mês (MMMM) ou AM/PM não tem como ser mascarado; ali o campo fica livre e vale a
leitura tolerante, que aceita 25/12/26, 25-12-2026, 25122026, 2512 (ano corrente) e
12/25/2026 2:05 pm. Digitar só a data num campo com hora mantém a hora que já estava escolhida.
Clicar no mês do cabeçalho troca para a grade de meses, e clicar no ano, para a de anos — assim uma data de nascimento não custa trinta cliques na seta.
No celular
O painel é o mesmo do desktop, adaptado ao toque: texto de 16px, células maiores, um mês embaixo do outro no período e os atalhos numa faixa que rola na horizontal.
Os 16px não são estética: o Safari do iOS dá zoom na página inteira ao focar campo com fonte menor. Pelo mesmo motivo, em tela estreita com toque o campo não recebe foco nem máscara — tocar abre o painel, e o teclado do sistema não sobe para cobrir o calendário.
O seletor do próprio sistema é opcional:
native: false // padrão: o painel em todo lugar
native: 'auto' // o seletor do sistema onde o ponteiro é de toque
native: true // sempre o seletor do sistema — por atributo, data-native="true"O modo nativo não troca o type do seu campo
Trocar para type="date" fazia todo CSS escrito como input[type=text] parar de casar, e o campo virava um input cru do navegador. Agora um input nativo transparente fica por cima, do tamanho do campo, e o POST continua sendo o do escondido. Período nunca usa o nativo: não existe seletor de intervalo em HTML.
Teclado e acessibilidade
O campo anuncia que abre um dialog, a grade é role="grid" e cada dia tem
aria-selected e o nome por extenso — "sexta-feira, 25 de dezembro de 2026". Chegar de Tab não abre o painel.
| Tecla | Onde | Ação |
|---|---|---|
↓ | Campo | Abre o calendário |
Espaço | Campo vazio | Abre o calendário |
Enter | Campo, painel aberto | Confirma o que foi digitado e fecha |
← → | Grade | Dia anterior e seguinte |
↑ ↓ | Grade | Mesmo dia na semana anterior e na seguinte |
PageUp PageDown | Grade | Mês anterior e seguinte; com Shift, ano |
Home End | Grade | Início e fim da semana |
Enter Espaço | Grade | Escolhe o dia |
↑ ↓ | Coluna de hora | Valor anterior e seguinte da coluna |
Home End | Coluna de hora | Primeiro e último valor da coluna |
Enter Espaço | Coluna de hora | Escolhe o valor |
Tab | Campo, painel aberto | Segue para o próximo campo e fecha o painel |
Tab | Dentro do painel | Anda entre os controles sem sair do painel; cada coluna de hora é uma parada só |
Esc | Campo ou painel | Fecha, descarta o que não foi confirmado e devolve o foco ao campo |
Por que Tab não abre, e Enter também não
Quem tabula por um formulário até o botão de salvar não deveria levar um calendário na cara a cada campo, cobrindo o seguinte — era o que empilhava painéis. E este é um campo de texto dentro de um <form>: Enter ali envia o formulário, que é o que se espera depois de digitar a data. Por isso quem abre é o Espaço, e só com o campo vazio, porque com hora se digita 07/09/2026 14:30.
Clique fora fecha sem roubar o foco de onde se clicou; um período escolhido pela metade é descartado ao fechar, porque não
existe meio intervalo, e o período que já estava escolhido volta. Com o botão Aplicar — o padrão com hora, ou
autoApply: false —, dia, hora e atalho ficam pendentes: só o Aplicar confirma e dispara tucano:change,
uma vez, e fechar com Esc ou clique fora descarta a escolha. Sem ele, cada escolha vale na hora. Com prefers-reduced-motion o painel só esmaece, sem deslizar.
API
Gerada do código a cada build — se algo não está aqui, não existe.
[data-tuc-datepicker]new Tucano.DatePicker(alvo, opcoes)data-format data-iso-name data-locale data-max data-min data-minute-step data-mode data-months data-native data-placement data-presets data-seconds data-time data-week-numbersgetValue setValue clear open close toggle destroytucano:changeOpções
| Opção | Padrão | Para quê |
|---|---|---|
mode | 'single' | 'single' | 'range' |
time | false | true habilita seletor de hora |
seconds | false | |
minuteStep | 5 | |
locale | undefined | default: locale do documento/navegador |
format | undefined | default: padrao numerico do locale |
firstDayOfWeek | undefined | |
months | undefined | default: 2 em range, 1 em single |
min | null | |
max | null | |
disabledDates | null | (date) => boolean |
presets | false | atalhos de periodo (Hoje, Ultimos 7 dias...): opt-in |
autoApply | undefined | default: true sem hora, false com hora. Sem autoApply, a escolha so vale no Aplicar |
clearable | true | |
weekNumbers | false | |
placement | 'bottom-center' | centralizado no campo; as bordas da tela ainda mandam |
appendTo | undefined | |
isoName | undefined | name do input hidden com o valor ISO |
native | false | |
onChange | null | |
onOpen | null | |
onClose | null |