HextaUI

Calendar

Uma grade de datas para seleção única, de intervalo e múltipla, com meses deslizantes, prévias de intervalo e dias com tamanho adequado ao toque.

October 2026
"use client"

import * as React from "react"
import { format } from "date-fns"

import { Calendar } from "@/components/ui/calendar"

export function CalendarDemo() {
  const [date, setDate] = React.useState<Date>()

  return (
    <div className="w-fit max-w-full overflow-x-auto rounded-xl border">
      <Calendar
        mode="single"
        selected={date}
        onSelect={setDate}
        footer={date ? `Selected ${format(date, "PPP")}.` : "Pick a day."}
      />
    </div>
  )
}
pnpm dlx shadcn@latest add https://hextaui.com/r/calendar.json

Adiciona o componente, os tokens de tema do HextaUI e quaisquer componentes do HextaUI dos quais ele depende.

import { Calendar } from "@/components/ui/calendar"
const [date, setDate] = React.useState<Date>()

<Calendar mode="single" selected={date} onSelect={setDate} />

Calendar envolve o <DayPicker /> do react-day-picker, então toda prop do DayPicker funciona como documentado em daypicker.dev. O HextaUI adiciona estilo, transições de mês, uma prévia de intervalo e um today seguro para hidratação.

Intervalo

Com mode="range", depois do primeiro clique, passar o mouse ou focar um dia mostra uma prévia do intervalo que o próximo clique selecionará. numberOfMonths exibe meses lado a lado, empilhados em telas estreitas.

"use client"

import * as React from "react"
import { format } from "date-fns"
import type { DateRange } from "react-day-picker"

import { Calendar } from "@/components/ui/calendar"

export function CalendarRange() {
  const [range, setRange] = React.useState<DateRange>()

  return (
    <div className="max-w-full overflow-x-auto rounded-xl border">
      <Calendar
        mode="range"
        numberOfMonths={2}
        selected={range}
        onSelect={setRange}
        footer={
          range?.from && range.to
            ? `${format(range.from, "PP")} – ${format(range.to, "PP")}`
            : range?.from
              ? "Pick an end date."
              : "Pick a start date."
        }
      />
    </div>
  )
}

Limites do intervalo

min e max limitam o tamanho do intervalo em dias. Com excludeDisabled, um intervalo que incluiria um dia desativado recomeça.

import { Calendar } from "@/components/ui/calendar"

export function CalendarRangeLimits() {
  return (
    <div className="w-fit max-w-full overflow-x-auto rounded-xl border">
      <Calendar
        mode="range"
        min={2}
        max={7}
        disabled={{ dayOfWeek: [0, 6] }}
        excludeDisabled
        footer="Weekdays only, 2–7 days."
      />
    </div>
  )
}

Múltiplo

mode="multiple" alterna dias individuais. max limita quantos podem ser selecionados.

import { Calendar } from "@/components/ui/calendar"

export function CalendarMultiple() {
  return (
    <div className="w-fit max-w-full overflow-x-auto rounded-xl border">
      <Calendar mode="multiple" max={5} footer="Pick up to five days." />
    </div>
  )
}

Dropdowns de mês e ano

captionLayout="dropdown" substitui a legenda por selects nativos, então os celulares ganham o próprio seletor. Defina startMonth e endMonth para limitar os anos.

import { Calendar } from "@/components/ui/calendar"

export function CalendarDropdowns() {
  return (
    <div className="w-fit max-w-full overflow-x-auto rounded-xl border">
      <Calendar
        mode="single"
        captionLayout="dropdown"
        startMonth={new Date(1940, 0)}
        endMonth={new Date(2035, 11)}
        defaultMonth={new Date(1995, 5)}
      />
    </div>
  )
}

Limitado

A navegação para em startMonth e endMonth, e disabled bloqueia os dias fora da janela. useToday() fornece um today que é seguro de usar durante a renderização no servidor.

"use client"

import { addDays, startOfMonth } from "date-fns"

import { Calendar } from "@/components/ui/calendar"
import { useToday } from "@/hooks/use-today"

export function CalendarBounded() {
  const today = useToday()
  const last = today && addDays(today, 45)

  return (
    <div className="w-fit max-w-full overflow-x-auto rounded-xl border">
      <Calendar
        mode="single"
        startMonth={today && startOfMonth(today)}
        endMonth={last && startOfMonth(last)}
        disabled={
          today && last ? [{ before: today }, { after: last }] : undefined
        }
        footer="Bookable for the next 45 days."
      />
    </div>
  )
}

Mês controlado

Passe month e onMonthChange para controlar o mês visível. Saltos deslizam na direção do deslocamento e a altura se ajusta suavemente entre linhas de 5 e 6 semanas.

"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import { Calendar } from "@/components/ui/calendar"
import { useToday } from "@/hooks/use-today"

export function CalendarControlled() {
  const today = useToday()
  const [month, setMonth] = React.useState<Date>()
  const [date, setDate] = React.useState<Date>()
  const shown = month ?? today

  return (
    <div className="flex max-w-full min-w-0 flex-col items-center gap-3">
      <div className="w-fit max-w-full overflow-x-auto rounded-xl border">
        <Calendar
          mode="single"
          month={shown}
          onMonthChange={setMonth}
          selected={date}
          onSelect={setDate}
        />
      </div>
      <div className="flex gap-2">
        <Button
          variant="outline"
          size="sm"
          onClick={() => {
            const now = new Date()
            setMonth(now)
            setDate(now)
          }}
        >
          Today
        </Button>
        <Button
          variant="outline"
          size="sm"
          onClick={() => {
            const base = shown ?? new Date()
            setMonth(new Date(base.getFullYear() + 1, base.getMonth()))
          }}
        >
          Next year
        </Button>
      </div>
    </div>
  )
}

Números de semana

showWeekNumber adiciona uma coluna de semana. ISOWeek usa a numeração ISO, começando na segunda-feira. showOutsideDays={false} oculta os dias de outros meses.

import { Calendar } from "@/components/ui/calendar"

export function CalendarWeekNumbers() {
  return (
    <div className="w-fit max-w-full overflow-x-auto rounded-xl border">
      <Calendar mode="range" showWeekNumber ISOWeek showOutsideDays={false} />
    </div>
  )
}

Today fixo

Passe today para fixar o dia destacado, em testes ou outro fuso horário. animate={false} desativa as transições de mês.

"use client"

import * as React from "react"
import type { DateRange } from "react-day-picker"

import { Calendar } from "@/components/ui/calendar"

export function CalendarFixedToday() {
  const [range, setRange] = React.useState<DateRange | undefined>({
    from: new Date(2026, 1, 9),
    to: new Date(2026, 1, 17),
  })

  return (
    <div className="w-fit max-w-full overflow-x-auto rounded-xl border">
      <Calendar
        mode="range"
        today={new Date(2026, 1, 11)}
        defaultMonth={new Date(2026, 1)}
        selected={range}
        onSelect={setRange}
        animate={false}
      />
    </div>
  )
}

Dentro de uma sheet

Dentro de um sheet, popover ou diálogo, o calendário abre mão do próprio fundo e se mistura com a superfície.

"use client"

import * as React from "react"
import { format } from "date-fns"

import { Button } from "@/components/ui/button"
import { Calendar } from "@/components/ui/calendar"
import {
  Sheet,
  SheetBody,
  SheetContent,
  SheetDescription,
  SheetHeader,
  SheetTitle,
  SheetTrigger,
} from "@/components/ui/sheet"

export function CalendarSheet() {
  const [date, setDate] = React.useState<Date>()

  return (
    <Sheet>
      <SheetTrigger render={<Button variant="outline" />}>
        {date ? format(date, "PPP") : "Choose a date"}
      </SheetTrigger>
      <SheetContent>
        <SheetHeader>
          <SheetTitle>Schedule</SheetTitle>
          <SheetDescription>Pick a delivery day.</SheetDescription>
        </SheetHeader>
        <SheetBody>
          <Calendar mode="single" selected={date} onSelect={setDate} />
        </SheetBody>
      </SheetContent>
    </Sheet>
  )
}

Da direita para a esquerda

Passe um locale de react-day-picker/locale e dir="rtl". As setas, a navegação e a direção do deslize são todas espelhadas. Dentro de um DirectionProvider com dir="rtl", a direção é detectada automaticamente.

"use client"

import { arSA } from "react-day-picker/locale"

import { Calendar } from "@/components/ui/calendar"

export function CalendarRtl() {
  return (
    <div dir="rtl" className="max-w-full min-w-0">
      <div className="w-fit max-w-full overflow-x-auto rounded-xl border">
        <Calendar mode="range" locale={arSA} dir="rtl" />
      </div>
    </div>
  )
}

Foque um dia e use estas teclas. Passar do mês visível muda o mês.

TeclaAção
←→Dia anterior ou seguinte. Invertido em layouts da direita para a esquerda.
↑↓O mesmo dia da semana anterior ou seguinte.
Shift←→Mês anterior ou seguinte.
Shift↑↓Ano anterior ou seguinte.
Page UpPage DownMês anterior ou seguinte.
ShiftPage UpPage DownAno anterior ou seguinte.
HomePrimeiro dia da semana.
EndÚltimo dia da semana.
EnterSpaceSeleciona o dia em foco.
  • O mês é uma grade. Cada dia é um botão com um rótulo de data completo, e os dias selecionados definem aria-selected.
  • Mudanças de mês feitas pelo teclado pulam o deslize e apenas fazem fade, para que o foco nunca se mova sob uma grade em movimento.
  • Com movimento reduzido, as mudanças de mês aparecem com fade e a mudança de altura é instantânea.
  • Em telas sensíveis ao toque, as células de dia crescem para 44px.

Aceita todas as props de <DayPicker />. Os padrões abaixo diferem dos do DayPicker ou são adicionados pelo HextaUI.

PropTipoPadrão
modeSem um mode, os dias não são selecionáveis.
"single" | "multiple" | "range"–
selectedCorresponde ao mode.
Date | Date[] | DateRange–
onSelect
(selected, triggerDate, modifiers, event) => void–
requiredImpede desmarcar a última seleção.
boolean–
minMínimo de dias em um intervalo, ou selecionados no modo multiple.
number–
maxMáximo de dias em um intervalo, ou selecionados no modo multiple.
number–
excludeDisabledModo de intervalo.
boolean–
disabled
Matcher | Matcher[]–
monthMês controlado.
Date–
defaultMonth
Date–
onMonthChange
(month: Date) => void–
startMonth
Date–
endMonth
Date–
numberOfMonths
number1
captionLayout
"label" | "dropdown" | "dropdown-months" | "dropdown-years""label"
navLayoutPadrão do HextaUI. As setas ficam em cada lado da legenda.
"around" | "after""around"
showOutsideDaysPadrão do HextaUI.
booleantrue
animateTransições de deslize e de altura do mês. Padrão do HextaUI.
booleantrue
buttonVariantVariant dos botões de anterior e seguinte.
Button variant"ghost"
showWeekNumber
booleanfalse
ISOWeek
booleanfalse
weekStartsOn
0 | 1 | 2 | 3 | 4 | 5 | 6–
fixedWeeks
booleanfalse
todayO padrão é o today do cliente, mantido em sincronia na virada da meia-noite e na hidratação.
Date–
timeZone
string–
locale
Partial<DayPickerLocale>–
dir
"ltr" | "rtl"–
footer
ReactNode–
AtributoDescrição
data-slot="calendar"Seleciona a raiz do calendário no CSS.
--cell-sizeTamanho da célula de dia. 36px, ou 44px em telas sensíveis ao toque.
--cell-radiusRaio dos cantos das células e dos botões de dia.
data-slot="calendar-day"Células de dia. Carregam data-selected, data-disabled, data-outside, data-today, data-hidden e data-focused.
data-previewNas células de dia: início, meio ou fim da prévia do intervalo sob o mouse.
data-range-middleNas células de dia dentro de um intervalo selecionado.

O botão dentro de cada dia. Passe o seu para components={{ DayButton }} e reutilize este para manter o estilo.

AtributoDescrição
data-slot="calendar-day-button"Seleciona os botões de dia no CSS.
data-dayA data ISO, como 2026-10-03.
data-todayPresente em today, excluindo os dias de fora.
data-selected-singleSelecionado fora de um intervalo.
data-range-startPrimeiro dia do intervalo.
data-range-middleUm dia dentro do intervalo.
data-range-endÚltimo dia do intervalo.

Retorna today como um Date no cliente e undefined durante a renderização no servidor, então limites construídos a partir dele nunca causam divergência de hidratação. Atualiza à meia-noite. Veja o guia do useToday.