HextaUI

Calendar

Ein Datumsraster für Einzel-, Bereichs- und Mehrfachauswahl, mit gleitenden Monaten, Bereichsvorschau und Tagen in Touch-Größe.

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

Fügt die Komponente, die HextaUI-Theme-Tokens und alle HextaUI-Komponenten hinzu, von denen sie abhängt.

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

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

Calendar umschließt das <DayPicker /> von react-day-picker, sodass jede DayPicker-Prop wie auf daypicker.dev dokumentiert funktioniert. HextaUI ergänzt Styling, Monatsübergänge, eine Bereichsvorschau und ein hydration-sicheres Heute.

Range

Mit mode="range" zeigt das Hovern oder Fokussieren eines Tages nach dem ersten Klick eine Vorschau des Bereichs, den der nächste Klick auswählt. numberOfMonths zeigt Monate nebeneinander, auf schmalen Bildschirmen gestapelt.

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

Bereichsgrenzen

min und max begrenzen die Länge des Bereichs in Tagen. Mit excludeDisabled beginnt ein Bereich, der einen deaktivierten Tag enthalten würde, stattdessen von vorn.

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

Mehrfach

mode="multiple" schaltet einzelne Tage um. max begrenzt, wie viele ausgewählt werden können.

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 für Monat und Jahr

captionLayout="dropdown" ersetzt die Überschrift durch native Selects, sodass Smartphones ihre eigene Auswahl erhalten. Setze startMonth und endMonth, um die Jahre zu begrenzen.

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

Begrenzt

Die Navigation stoppt bei startMonth und endMonth, und disabled sperrt Tage außerhalb des Fensters. useToday() liefert ein Heute, das beim Server-Rendering sicher verwendet werden kann.

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

Gesteuerter Monat

Übergib month und onMonthChange, um den sichtbaren Monat zu steuern. Sprünge gleiten in Bewegungsrichtung, und die Höhe passt sich sanft zwischen 5 und 6 Wochenzeilen an.

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

Kalenderwochen

showWeekNumber fügt eine Wochenspalte hinzu. ISOWeek verwendet die ISO-Zählung, beginnend am Montag. showOutsideDays={false} blendet Tage anderer Monate aus.

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

Festes Heute

Übergib today, um den hervorgehobenen Tag festzulegen, für Tests oder eine andere Zeitzone. animate={false} schaltet Monatsübergänge ab.

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

In einem Sheet

In einem Sheet, Popover oder Dialog verwirft der Kalender seinen eigenen Hintergrund und geht in die Fläche über.

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

Rechts nach links

Übergib ein locale aus react-day-picker/locale und dir="rtl". Pfeile, Navigation und Gleitrichtung werden alle gespiegelt. Innerhalb eines DirectionProvider mit dir="rtl" wird die Richtung automatisch übernommen.

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

Fokussiere einen Tag und nutze dann diese Tasten. Das Verlassen des sichtbaren Monats wechselt den Monat.

TasteAktion
←→Vorheriger oder nächster Tag. In Rechts-nach-links-Layouts umgekehrt.
↑↓Derselbe Tag der vorherigen oder nächsten Woche.
Shift←→Vorheriger oder nächster Monat.
Shift↑↓Vorheriges oder nächstes Jahr.
Page UpPage DownVorheriger oder nächster Monat.
ShiftPage UpPage DownVorheriges oder nächstes Jahr.
HomeErster Tag der Woche.
EndLetzter Tag der Woche.
EnterSpaceWählt den fokussierten Tag aus.
  • Der Monat ist ein Raster. Jeder Tag ist ein Button mit einem vollständigen Datumslabel, und ausgewählte Tage setzen aria-selected.
  • Monatswechsel per Tastatur überspringen das Gleiten und blenden nur über, sodass der Fokus sich nie unter einem bewegten Raster verschiebt.
  • Bei reduzierter Bewegung blenden Monatswechsel über, und die Höhenänderung erfolgt sofort.
  • Auf Touchscreens wachsen die Tageszellen auf 44 px.

Akzeptiert alle <DayPicker />-Props. Die folgenden Standardwerte weichen von denen von DayPicker ab oder wurden von HextaUI hinzugefügt.

PropTypStandard
modeOhne Modus sind Tage nicht auswählbar.
"single" | "multiple" | "range"–
selectedEntspricht dem Modus.
Date | Date[] | DateRange–
onSelect
(selected, triggerDate, modifiers, event) => void–
requiredVerhindert das Abwählen der letzten Auswahl.
boolean–
minMindestanzahl Tage in einem Bereich oder im Modus multiple ausgewählt.
number–
maxMaximale Anzahl Tage in einem Bereich oder im Modus multiple ausgewählt.
number–
excludeDisabledBereichsmodus.
boolean–
disabled
Matcher | Matcher[]–
monthGesteuerter Monat.
Date–
defaultMonth
Date–
onMonthChange
(month: Date) => void–
startMonth
Date–
endMonth
Date–
numberOfMonths
number1
captionLayout
"label" | "dropdown" | "dropdown-months" | "dropdown-years""label"
navLayoutHextaUI-Standard. Die Pfeile sitzen zu beiden Seiten der Überschrift.
"around" | "after""around"
showOutsideDaysHextaUI-Standard.
booleantrue
animateMonatsgleiten und Höhenübergänge. HextaUI-Standard.
booleantrue
buttonVariantVariante der Buttons für zurück und weiter.
Button variant"ghost"
showWeekNumber
booleanfalse
ISOWeek
booleanfalse
weekStartsOn
0 | 1 | 2 | 3 | 4 | 5 | 6–
fixedWeeks
booleanfalse
todayStandardmäßig das heutige Datum des Clients, über Mitternacht und Hydration hinweg synchron gehalten.
Date–
timeZone
string–
locale
Partial<DayPickerLocale>–
dir
"ltr" | "rtl"–
footer
ReactNode–
AttributBeschreibung
data-slot="calendar"Die Kalender-Root in CSS ansprechen.
--cell-sizeGröße der Tageszellen. 36 px, auf Touchscreens 44 px.
--cell-radiusEckenradius der Tageszellen und Buttons.
data-slot="calendar-day"Tageszellen. Tragen data-selected, data-disabled, data-outside, data-today, data-hidden und data-focused.
data-previewAuf Tageszellen: Anfang, Mitte oder Ende der Bereichsvorschau beim Hovern.
data-range-middleAuf Tageszellen in einem ausgewählten Bereich.

Der Button in jedem Tag. Übergib deinen eigenen an components={{ DayButton }} und verwende diesen weiter, um das Styling beizubehalten.

AttributBeschreibung
data-slot="calendar-day-button"Tag-Buttons in CSS ansprechen.
data-dayDas ISO-Datum, etwa 2026-10-03.
data-todayVorhanden am heutigen Tag, ohne Tage außerhalb des Monats.
data-selected-singleAusgewählt, außerhalb eines Bereichs.
data-range-startErster Tag des Bereichs.
data-range-middleEin Tag innerhalb des Bereichs.
data-range-endLetzter Tag des Bereichs.

Gibt das heutige Datum als Date auf dem Client zurück und undefined beim Server-Rendering, sodass daraus gebildete Grenzen nie zu einem Hydration-Mismatch führen. Es aktualisiert sich um Mitternacht. Siehe die useToday-Anleitung.