HextaUI

Calendar

Une grille de dates pour la sélection simple, de plage et multiple, avec des mois qui défilent, des aperçus de plage et des jours à taille tactile.

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

Ajoute le composant, les tokens de thème HextaUI et les composants HextaUI dont il dépend.

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

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

Calendar enveloppe le <DayPicker /> de react-day-picker : toutes les props de DayPicker fonctionnent comme documenté sur daypicker.dev. HextaUI ajoute le style, les transitions de mois, un aperçu de plage et un today compatible avec l’hydratation.

Plage

Avec mode="range", après le premier clic, survoler ou mettre le focus sur un jour affiche un aperçu de la plage que le prochain clic sélectionnera. numberOfMonths affiche les mois côte à côte, empilés sur les écrans étroits.

"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 de la plage

min et max limitent la longueur de la plage en jours. Avec excludeDisabled, une plage qui inclurait un jour désactivé repart de zéro.

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

Multiple

mode="multiple" bascule les jours individuellement. max plafonne le nombre de jours sélectionnables.

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

Listes déroulantes du mois et de l’année

captionLayout="dropdown" remplace la légende par des select natifs, ce qui offre aux téléphones leur propre sélecteur. Définissez startMonth et endMonth pour borner les années.

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

Bornée

La navigation s’arrête à startMonth et endMonth, et disabled bloque les jours en dehors de la fenêtre. useToday() fournit un today utilisable sans risque pendant le rendu serveur.

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

Mois contrôlé

Passez month et onMonthChange pour piloter le mois visible. Les sauts glissent dans le sens du déplacement et la hauteur s’adapte en douceur entre 5 et 6 lignes de semaines.

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

Numéros de semaine

showWeekNumber ajoute une colonne de semaines. ISOWeek utilise la numérotation ISO, commençant le lundi. showOutsideDays={false} masque les jours des autres mois.

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

Aujourd’hui fixe

Passez today pour figer le jour mis en évidence, pour les tests ou un autre fuseau horaire. animate={false} désactive les transitions de mois.

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

Dans une sheet

Dans une sheet, un popover ou une boîte de dialogue, le calendrier abandonne son propre fond et se fond dans la surface.

"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 droite à gauche

Passez une locale de react-day-picker/locale et dir="rtl". Les flèches, la navigation et le sens du glissement s’inversent. Dans un DirectionProvider dir="rtl", la direction est détectée automatiquement.

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

Placez le focus sur un jour, puis utilisez ces touches. Dépasser le mois visible change de mois.

ToucheAction
←→Jour précédent ou suivant. Inversé dans les mises en page de droite à gauche.
↑↓Le même jour de la semaine précédente ou suivante.
Shift←→Mois précédent ou suivant.
Shift↑↓Année précédente ou suivante.
Page UpPage DownMois précédent ou suivant.
ShiftPage UpPage DownAnnée précédente ou suivante.
HomePremier jour de la semaine.
EndDernier jour de la semaine.
EnterSpaceSélectionne le jour ayant le focus.
  • Le mois est une grille. Chaque jour est un bouton avec une étiquette de date complète, et les jours sélectionnés définissent aria-selected.
  • Les changements de mois au clavier sautent le glissement et ne font que se fondre, pour que le focus ne se retrouve jamais sous une grille en mouvement.
  • Avec la réduction des animations, les changements de mois se fondent et le changement de hauteur est instantané.
  • Sur écran tactile, les cellules de jour passent à 44 px.

Accepte toutes les props de <DayPicker />. Les valeurs par défaut ci-dessous diffèrent de celles de DayPicker ou sont ajoutées par HextaUI.

PropTypePar défaut
modeSans mode, les jours ne sont pas sélectionnables.
"single" | "multiple" | "range"–
selectedCorrespond au mode.
Date | Date[] | DateRange–
onSelect
(selected, triggerDate, modifiers, event) => void–
requiredEmpêche de désélectionner la dernière sélection.
boolean–
minNombre minimal de jours dans une plage, ou sélectionnés en mode multiple.
number–
maxNombre maximal de jours dans une plage, ou sélectionnés en mode multiple.
number–
excludeDisabledMode plage.
boolean–
disabled
Matcher | Matcher[]–
monthMois contrôlé.
Date–
defaultMonth
Date–
onMonthChange
(month: Date) => void–
startMonth
Date–
endMonth
Date–
numberOfMonths
number1
captionLayout
"label" | "dropdown" | "dropdown-months" | "dropdown-years""label"
navLayoutValeur par défaut de HextaUI. Les flèches se placent de part et d’autre de la légende.
"around" | "after""around"
showOutsideDaysValeur par défaut de HextaUI.
booleantrue
animateTransitions de glissement du mois et de hauteur. Valeur par défaut de HextaUI.
booleantrue
buttonVariantVariante des boutons précédent et suivant.
Button variant"ghost"
showWeekNumber
booleanfalse
ISOWeek
booleanfalse
weekStartsOn
0 | 1 | 2 | 3 | 4 | 5 | 6–
fixedWeeks
booleanfalse
todayPar défaut, le jour actuel du client, synchronisé à travers minuit et l’hydratation.
Date–
timeZone
string–
locale
Partial<DayPickerLocale>–
dir
"ltr" | "rtl"–
footer
ReactNode–
AttributDescription
data-slot="calendar"Ciblez la racine du calendrier en CSS.
--cell-sizeTaille des cellules de jour. 36 px, ou 44 px sur écran tactile.
--cell-radiusRayon des coins des cellules et des boutons de jour.
data-slot="calendar-day"Cellules de jour. Portent data-selected, data-disabled, data-outside, data-today, data-hidden et data-focused.
data-previewSur les cellules de jour : début, milieu ou fin de l’aperçu de la plage survolée.
data-range-middleSur les cellules de jour dans une plage sélectionnée.

Le bouton dans chaque jour. Passez le vôtre à components={{ DayButton }} et réutilisez celui-ci pour conserver le style.

AttributDescription
data-slot="calendar-day-button"Ciblez les boutons de jour en CSS.
data-dayLa date ISO, comme 2026-10-03.
data-todayPrésent sur aujourd’hui, hors jours extérieurs.
data-selected-singleSélectionné en dehors d’une plage.
data-range-startPremier jour de la plage.
data-range-middleUn jour dans la plage.
data-range-endDernier jour de la plage.

Retourne aujourd’hui sous forme de Date côté client et undefined pendant le rendu serveur : les bornes construites à partir de lui ne provoquent donc jamais de désaccord d’hydratation. Il se met à jour à minuit. Consultez le guide de useToday.