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
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
| Parte | Regra |
|---|---|
| Trigger | Mesma altura de Input/Select; ícone à esquerda; valor formatado |
| Single date | Um mês visível; hoje marcado sem competir com seleção |
| Range | Dois meses lado a lado em desktop; presets para intervalos comuns |
| Mobile | Popover vira superfície adequada ao viewport quando necessário |
| Locale | pt-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
disablede não devem parecer selecionáveis. - Range precisa comunicar início e fim em texto, não só por cor.
Regras
- Não esconda o período ativo; ele muda a leitura dos dados.
- Range deve indicar início e fim com clareza.
- Hoje pode ser marcado, mas não deve competir com seleção.
- Presets precisam refletir as queries mais comuns.
- Datas em logs/auditoria devem preservar timezone quando relevante.
- Menos de quatro opções fixas viram Radio/Segmented, não calendário.
- Estado inválido precisa explicar limite, não apenas bloquear seleção.
Last updated on