Skip to Content
ComponentsAtomsCalendar

Calendar

Atom

Calendar resolve seleção de data. DatePicker é a experiência de trigger + popover que usa Calendar.

Use Calendar para filtros temporais, relatórios, data de incidente e deadlines. Em produto, ele normalmente aparece como DatePicker: trigger de 36px, ícone de calendário e popover com calendário. Para 24h / 7d / 30d, prefira Radio/Segmented.

Exemplos copiáveis

React

import * as React from "react" type CalendarDay = { date: Date outside?: boolean disabled?: boolean } type PulsoCalendarProps = { label: string monthLabel: string days: CalendarDay[] selectedDate?: Date onSelectDate: (date: Date) => void } const formatter = new Intl.DateTimeFormat("pt-BR", { day: "2-digit", month: "short", year: "numeric", }) function isSameDay(a?: Date, b?: Date) { return Boolean( a && b && a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate(), ) } export function PulsoCalendar({ label, monthLabel, days, selectedDate, onSelectDate, }: PulsoCalendarProps) { return ( <section aria-label={label} className="w-[280px] rounded-[var(--radius-md)] border border-[var(--border-strong)] bg-[var(--popover)] p-3 shadow-[var(--shadow-md)]" > <header className="mb-3 flex items-center justify-between gap-3"> <h2 className="text-sm font-semibold text-[var(--fg-strong)]">{monthLabel}</h2> <span className="font-mono text-[10px] uppercase tracking-[var(--tracking-kicker)] text-[var(--fg-subtle)]"> {selectedDate ? formatter.format(selectedDate) : "Sem data"} </span> </header> <div className="grid grid-cols-7 gap-0.5 text-center font-mono text-[10px] uppercase tracking-[0.05em] text-[var(--fg-subtle)]"> {["D", "S", "T", "Q", "Q", "S", "S"].map((dayName, index) => ( <div key={`${dayName}-${index}`}>{dayName}</div> ))} </div> <div className="mt-1 grid grid-cols-7 gap-0.5" role="grid" aria-label={monthLabel}> {days.map((day) => { const selected = isSameDay(day.date, selectedDate) return ( <button key={day.date.toISOString()} type="button" disabled={day.disabled} aria-selected={selected} onClick={() => onSelectDate(day.date)} className={[ "h-8 rounded-[var(--radius-sm)] text-xs tabular-nums transition-colors duration-[var(--motion-duration-fast)] ease-[var(--motion-easing)]", "focus-visible:outline focus-visible:outline-[var(--ring-width)] focus-visible:outline-offset-2 focus-visible:outline-[var(--ring)]", selected ? "bg-[var(--primary)] font-semibold text-[var(--primary-foreground)]" : "text-[var(--fg)] hover:bg-[var(--muted)]", day.outside ? "text-[var(--fg-disabled)]" : "", day.disabled ? "cursor-not-allowed opacity-50" : "", ].filter(Boolean).join(" ")} > {day.date.getDate()} </button> ) })} </div> </section> ) }
const [selectedDate, setSelectedDate] = React.useState(new Date(2026, 4, 14)) <PulsoCalendar label="Data do incidente" monthLabel="Maio 2026" selectedDate={selectedDate} onSelectDate={setSelectedDate} days={daysForMay2026} />

DatePicker contract

ParteRegra
TriggerMesma altura de Input/Select; ícone à esquerda; valor formatado
Single dateUm mês visível; hoje marcado sem competir com seleção
RangeDois meses lado a lado em desktop; presets para intervalos comuns
MobilePopover vira superfície adequada ao viewport quando necessário
Localept-BR por padrão em produto Pulso

Quando usar

  • Filtros temporais em dashboards e relatórios.
  • Data de incidente, agendamento ou deadline.
  • Range com presets para queries comuns.
  • Revisão de períodos de auditoria.

Quando não usar

  • Para poucas opções fixas como 24h / 7d / 30d; use Radio/Segmented.
  • Para hora isolada; use input específico.
  • Para datas em logs sem timezone explícito.
  • Para categorias sem ordem temporal.

Acessibilidade

  • Trigger precisa anunciar valor selecionado e abrir o calendário por teclado.
  • Dia selecionado usa aria-selected.
  • Dias indisponíveis usam disabled e não devem parecer selecionáveis.
  • Range precisa comunicar início e fim em texto, não só por cor.

Regras

  1. Não esconda o período ativo; ele muda a leitura dos dados.
  2. Range deve indicar início e fim com clareza.
  3. Hoje pode ser marcado, mas não deve competir com seleção.
  4. Presets precisam refletir as queries mais comuns.
  5. Datas em logs/auditoria devem preservar timezone quando relevante.
  6. Menos de quatro opções fixas viram Radio/Segmented, não calendário.
  7. Estado inválido precisa explicar limite, não apenas bloquear seleção.
Last updated on