HextaUI

Calendar

A date grid for single, range and multiple selection, with sliding months, range previews and touch-sized days.

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

Adds the component, the HextaUI theme tokens and any HextaUI components it depends on.

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

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

Calendar wraps react-day-picker’s <DayPicker />, so every DayPicker prop works as documented on daypicker.dev. HextaUI adds styling, month transitions, a range preview and a hydration-safe today.

Range

With mode="range", after the first click hovering or focusing a day previews the range the next click will select. numberOfMonths shows months side by side, stacked on narrow screens.

October 2026
November 2026
"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>
  )
}

Range limits

min and max limit the range length in days. With excludeDisabled, a range that would include a disabled day starts over instead.

October 2026
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" toggles individual days. max caps how many can be selected.

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

Month and year dropdowns

captionLayout="dropdown" replaces the caption with native selects, so phones get their own picker. Set startMonth and endMonth to bound the years.

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

Bounded

Navigation stops at startMonth and endMonth, and disabled blocks days outside the window. useToday() gives a today that is safe to use during server rendering.

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

Controlled month

Pass month and onMonthChange to drive the visible month. Jumps slide in the direction of travel and the height eases between 5 and 6 week rows.

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

Week numbers

showWeekNumber adds a week column. ISOWeek uses ISO numbering, starting on Monday. showOutsideDays={false} hides days of other months.

October 2026
40
41
42
43
44
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>
  )
}

Fixed today

Pass today to pin the highlighted day, for tests or another time zone. animate={false} turns off month transitions.

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

Inside a sheet

Inside a sheet, popover or dialog the calendar drops its own background and blends with the 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>
  )
}

Right to left

Pass a locale from react-day-picker/locale and dir="rtl". Arrows, navigation and the slide direction all mirror. Inside a dir="rtl" DirectionProvider, the direction is picked up automatically.

أكتوبر 2026
"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>
  )
}

Focus a day, then use these keys. Moving past the visible month changes the month.

KeyAction
←→Previous or next day. Reversed in right-to-left layouts.
↑↓Same day of the previous or next week.
Shift←→Previous or next month.
Shift↑↓Previous or next year.
Page UpPage DownPrevious or next month.
ShiftPage UpPage DownPrevious or next year.
HomeFirst day of the week.
EndLast day of the week.
EnterSpaceSelects the focused day.
  • The month is a grid. Each day is a button with a full date label, and selected days set aria-selected.
  • Month changes made with the keyboard skip the slide and only fade, so focus never moves under a moving grid.
  • With reduced motion, month changes fade and the height change is instant.
  • On touch screens the day cells grow to 44px.

Accepts every <DayPicker /> prop. The defaults below differ from DayPicker’s or are added by HextaUI.

PropTypeDefault
modeWithout a mode, days are not selectable.
"single" | "multiple" | "range"–
selectedMatches the mode.
Date | Date[] | DateRange–
onSelect
(selected, triggerDate, modifiers, event) => void–
requiredPrevents deselecting the last selection.
boolean–
minMinimum days in a range, or selected in multiple mode.
number–
maxMaximum days in a range, or selected in multiple mode.
number–
excludeDisabledRange mode.
boolean–
disabled
Matcher | Matcher[]–
monthControlled month.
Date–
defaultMonth
Date–
onMonthChange
(month: Date) => void–
startMonth
Date–
endMonth
Date–
numberOfMonths
number1
captionLayout
"label" | "dropdown" | "dropdown-months" | "dropdown-years""label"
navLayoutHextaUI default. Arrows sit on either side of the caption.
"around" | "after""around"
showOutsideDaysHextaUI default.
booleantrue
animateMonth slide and height transitions. HextaUI default.
booleantrue
buttonVariantVariant of the previous and next buttons.
Button variant"ghost"
showWeekNumber
booleanfalse
ISOWeek
booleanfalse
weekStartsOn
0 | 1 | 2 | 3 | 4 | 5 | 6–
fixedWeeks
booleanfalse
todayDefaults to the client’s today, kept in sync across midnight and hydration.
Date–
timeZone
string–
locale
Partial<DayPickerLocale>–
dir
"ltr" | "rtl"–
footer
ReactNode–
AttributeDescription
data-slot="calendar"Target the calendar root in CSS.
--cell-sizeDay cell size. 36px, or 44px on touch screens.
--cell-radiusCorner radius of day cells and buttons.
data-slot="calendar-day"Day cells. Carry data-selected, data-disabled, data-outside, data-today, data-hidden and data-focused.
data-previewOn day cells: start, middle or end of the hovered range preview.
data-range-middleOn day cells inside a selected range.

The button inside each day. Pass your own to components={{ DayButton }} and reuse this one to keep the styling.

AttributeDescription
data-slot="calendar-day-button"Target day buttons in CSS.
data-dayThe ISO date, like 2026-10-03.
data-todayPresent on today, outside days excluded.
data-selected-singleSelected outside a range.
data-range-startFirst day of the range.
data-range-middleA day inside the range.
data-range-endLast day of the range.

Returns today as a Date on the client and undefined during server rendering, so bounds built from it never cause a hydration mismatch. It updates at midnight. See the useToday guide.