HextaUI

Calendar

Una cuadrícula de fechas para selección única, de rango y múltiple, con meses deslizantes, vistas previas de rango y días de tamaño táctil.

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

Añade el componente, los tokens del tema de HextaUI y los componentes de HextaUI de los que depende.

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

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

Calendar envuelve el <DayPicker /> de react-day-picker, así que todas las props de DayPicker funcionan tal como se documentan en daypicker.dev. HextaUI añade estilos, transiciones de mes, una vista previa del rango y un today seguro para la hidratación.

Rango

Con mode="range", tras el primer clic, pasar el cursor o dar foco a un día muestra una vista previa del rango que seleccionará el siguiente clic. numberOfMonths muestra los meses uno al lado del otro, apilados en pantallas estrechas.

"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>
  )
}

Límites del rango

min y max limitan la longitud del rango en días. Con excludeDisabled, un rango que incluiría un día deshabilitado se reinicia.

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últiple

mode="multiple" alterna días individuales. max limita cuántos se pueden seleccionar.

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>
  )
}

Desplegables de mes y año

captionLayout="dropdown" reemplaza el título por selects nativos, de modo que los teléfonos obtienen su propio selector. Define startMonth y endMonth para acotar los años.

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>
  )
}

Acotado

La navegación se detiene en startMonth y endMonth, y disabled bloquea los días fuera de la ventana. useToday() te da un today seguro para usar durante el renderizado en el 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>
  )
}

Mes controlado

Pasa month y onMonthChange para controlar el mes visible. Los saltos se deslizan en la dirección del recorrido y la altura se ajusta suavemente entre filas de 5 y 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 añade una columna de semana. ISOWeek usa la numeración ISO, que empieza en lunes. showOutsideDays={false} oculta los días de otros 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 fijo

Pasa today para fijar el día resaltado, para pruebas u otra zona horaria. animate={false} desactiva las transiciones de mes.

"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 una hoja

Dentro de un sheet, popover o diálogo, el calendario elimina su propio fondo y se funde con la superficie.

"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>
  )
}

De derecha a izquierda

Pasa un locale de react-day-picker/locale y dir="rtl". Las flechas, la navegación y la dirección del deslizamiento se reflejan. Dentro de un DirectionProvider con dir="rtl", la dirección se detecta automáticamente.

"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>
  )
}

Da foco a un día y usa estas teclas. Pasar del mes visible cambia el mes.

KeyAcción
←→Día anterior o siguiente. Invertido en diseños de derecha a izquierda.
↑↓El mismo día de la semana anterior o siguiente.
Shift←→Mes anterior o siguiente.
Shift↑↓Año anterior o siguiente.
Page UpPage DownMes anterior o siguiente.
ShiftPage UpPage DownAño anterior o siguiente.
HomePrimer día de la semana.
EndÚltimo día de la semana.
EnterSpaceSelecciona el día con foco.
  • El mes es una cuadrícula. Cada día es un botón con una etiqueta de fecha completa, y los días seleccionados definen aria-selected.
  • Los cambios de mes hechos con el teclado omiten el deslizamiento y solo hacen un fundido, para que el foco nunca se mueva bajo una cuadrícula en movimiento.
  • Con movimiento reducido, los cambios de mes se desvanecen y el cambio de altura es instantáneo.
  • En pantallas táctiles, las celdas de día crecen hasta 44px.

Acepta todas las props de <DayPicker />. Los valores por defecto de abajo difieren de los de DayPicker o los añade HextaUI.

PropTipoPredeterminado
modeSin un modo, los días no se pueden seleccionar.
"single" | "multiple" | "range"–
selectedCoincide con el modo.
Date | Date[] | DateRange–
onSelect
(selected, triggerDate, modifiers, event) => void–
requiredImpide deseleccionar la última selección.
boolean–
minMínimo de días en un rango, o seleccionados en modo multiple.
number–
maxMáximo de días en un rango, o seleccionados en modo multiple.
number–
excludeDisabledModo range.
boolean–
disabled
Matcher | Matcher[]–
monthMes controlado.
Date–
defaultMonth
Date–
onMonthChange
(month: Date) => void–
startMonth
Date–
endMonth
Date–
numberOfMonths
number1
captionLayout
"label" | "dropdown" | "dropdown-months" | "dropdown-years""label"
navLayoutValor por defecto de HextaUI. Las flechas van a cada lado del título.
"around" | "after""around"
showOutsideDaysValor por defecto de HextaUI.
booleantrue
animateTransiciones de deslizamiento de mes y de altura. Valor por defecto de HextaUI.
booleantrue
buttonVariantVariante de los botones de anterior y siguiente.
Button variant"ghost"
showWeekNumber
booleanfalse
ISOWeek
booleanfalse
weekStartsOn
0 | 1 | 2 | 3 | 4 | 5 | 6–
fixedWeeks
booleanfalse
todayPor defecto es el today del cliente, mantenido sincronizado a través de la medianoche y la hidratación.
Date–
timeZone
string–
locale
Partial<DayPickerLocale>–
dir
"ltr" | "rtl"–
footer
ReactNode–
AtributoDescripción
data-slot="calendar"Selecciona la raíz del calendario en CSS.
--cell-sizeTamaño de la celda de día. 36px, o 44px en pantallas táctiles.
--cell-radiusRadio de las esquinas de las celdas y botones de día.
data-slot="calendar-day"Celdas de día. Llevan data-selected, data-disabled, data-outside, data-today, data-hidden y data-focused.
data-previewEn las celdas de día: inicio, medio o fin de la vista previa del rango bajo el cursor.
data-range-middleEn las celdas de día dentro de un rango seleccionado.

El botón dentro de cada día. Pasa el tuyo a components={{ DayButton }} y reutiliza este para conservar el estilo.

AtributoDescripción
data-slot="calendar-day-button"Selecciona los botones de día en CSS.
data-dayLa fecha ISO, como 2026-10-03.
data-todayPresente en today, excluidos los días de fuera del mes.
data-selected-singleSeleccionado fuera de un rango.
data-range-startPrimer día del rango.
data-range-middleUn día dentro del rango.
data-range-endÚltimo día del rango.

Devuelve today como un Date en el cliente y undefined durante el renderizado en el servidor, así que los límites construidos a partir de él nunca causan una discrepancia de hidratación. Se actualiza a medianoche. Consulta la guía de useToday.