HextaUI

Date picker

A button that opens a calendar in a popover, or a bottom sheet on phones, for single dates and ranges.

import { DatePicker } from "@/components/ui/date-picker"

export function DatePickerDemo() {
  return <DatePicker />
}
pnpm dlx shadcn@latest add https://hextaui.com/r/date-picker.json

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

import { DatePicker, DateRangePicker } from "@/components/ui/date-picker"
const [date, setDate] = React.useState<Date | null>(null)

<DatePicker value={date} onValueChange={setDate} />

On screens narrower than 640px the calendar opens in a bottom sheet instead of a popover, so days stay large enough to tap.

Controlled

Pass value and onValueChange. Cleared values are null. The calendar always opens on the month of the selected date, and picking a day closes it and returns focus to the button.

null
"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import { DatePicker } from "@/components/ui/date-picker"

function daysFromToday(days: number) {
  const date = new Date()
  date.setHours(0, 0, 0, 0)
  date.setDate(date.getDate() + days)
  return date
}

const isoDay = new Intl.DateTimeFormat("en-CA", {
  year: "numeric",
  month: "2-digit",
  day: "2-digit",
})

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

  return (
    <div className="flex flex-wrap items-center justify-center gap-2">
      <DatePicker value={date} onValueChange={setDate} clearable />
      <Button variant="ghost" onClick={() => setDate(daysFromToday(0))}>
        Today
      </Button>
      <Button variant="ghost" onClick={() => setDate(daysFromToday(7))}>
        In a week
      </Button>
      <output className="text-sm text-muted-foreground tabular-nums">
        {date ? isoDay.format(date) : "null"}
      </output>
    </div>
  )
}

Range

<DateRangePicker /> shows two months on larger screens. The first click always starts a new range, even when one is set, and the second click finishes it in either order and closes the picker.

null
"use client"

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

import { DateRangePicker } from "@/components/ui/date-picker"

const isoDay = new Intl.DateTimeFormat("en-CA", {
  year: "numeric",
  month: "2-digit",
  day: "2-digit",
})

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

  return (
    <div className="flex flex-col items-center gap-2">
      <DateRangePicker value={range} onValueChange={setRange} clearable />
      <output className="text-sm text-muted-foreground tabular-nums">
        {range?.from && range.to
          ? `${isoDay.format(range.from)} → ${isoDay.format(range.to)}`
          : "null"}
      </output>
    </div>
  )
}

Disabled days

Pass any react-day-picker matcher as calendarProps.disabled. Here only future weekdays can be picked.

"use client"

import * as React from "react"

import { DatePicker } from "@/components/ui/date-picker"

function tomorrow() {
  const date = new Date()
  date.setHours(0, 0, 0, 0)
  date.setDate(date.getDate() + 1)
  return date
}

export function DatePickerDisabledDays() {
  const [firstDay] = React.useState(tomorrow)

  return (
    <DatePicker
      placeholder="Delivery date"
      calendarProps={{
        disabled: [{ before: firstDay }, { dayOfWeek: [0, 6] }],
      }}
    />
  )
}

Month and year dropdowns

For far-away dates like a birthday, set captionLayout: "dropdown" with a startMonth and endMonth. Label the trigger with a <label /> pointing at its id.

import { DatePicker } from "@/components/ui/date-picker"

export function DatePickerDropdowns() {
  return (
    <div className="flex flex-col gap-1.5">
      <label htmlFor="birthday" className="text-sm font-medium">
        Date of birth
      </label>
      <DatePicker
        id="birthday"
        placeholder="Select your birthday"
        calendarProps={{
          captionLayout: "dropdown",
          startMonth: new Date(1920, 0),
          endMonth: new Date(),
          disabled: { after: new Date() },
        }}
      />
    </div>
  )
}

In a form

With name, a single date is submitted as 2026-10-14 and a range as 2026-10-20/2026-10-24. Empty pickers submit an empty string.

Not submitted
"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import { DatePicker, DateRangePicker } from "@/components/ui/date-picker"

export function DatePickerForm() {
  const [submitted, setSubmitted] = React.useState<string>()

  return (
    <form
      className="flex flex-col items-start gap-3"
      onSubmit={(event) => {
        event.preventDefault()
        const data = new FormData(event.currentTarget)
        setSubmitted(JSON.stringify(Object.fromEntries(data)))
      }}
    >
      <DatePicker name="due" defaultValue={new Date(2026, 9, 14)} />
      <DateRangePicker
        name="stay"
        defaultValue={{
          from: new Date(2026, 9, 20),
          to: new Date(2026, 9, 24),
        }}
      />
      <Button type="submit">Submit</Button>
      <output className="text-sm break-all text-muted-foreground">
        {submitted ?? "Not submitted"}
      </output>
    </form>
  )
}

Disabled

A disabled trigger can still show a value.

import { DatePicker } from "@/components/ui/date-picker"

export function DatePickerDisabled() {
  return (
    <div className="flex flex-wrap justify-center gap-2">
      <DatePicker disabled />
      <DatePicker disabled defaultValue={new Date(2026, 0, 1)} />
    </div>
  )
}

Narrow container

The trigger never grows past its container. Long labels truncate instead of wrapping.

import { DateRangePicker } from "@/components/ui/date-picker"

export function DatePickerNarrow() {
  return (
    <div className="w-40">
      <DateRangePicker
        defaultValue={{
          from: new Date(2026, 0, 28),
          to: new Date(2026, 11, 3),
        }}
      />
    </div>
  )
}

Inside a sheet

The picker layers above the sheet, and Escape closes only the picker.

import { Button } from "@/components/ui/button"
import { DateRangePicker } from "@/components/ui/date-picker"
import {
  Sheet,
  SheetBody,
  SheetContent,
  SheetDescription,
  SheetHeader,
  SheetTitle,
  SheetTrigger,
} from "@/components/ui/sheet"

export function DatePickerInSheet() {
  return (
    <Sheet>
      <SheetTrigger render={<Button variant="outline" />}>
        Edit booking
      </SheetTrigger>
      <SheetContent>
        <SheetHeader>
          <SheetTitle>Edit booking</SheetTitle>
          <SheetDescription>
            The picker opens above the sheet and Escape closes only the picker.
          </SheetDescription>
        </SheetHeader>
        <SheetBody>
          <DateRangePicker />
        </SheetBody>
      </SheetContent>
    </Sheet>
  )
}

Right to left

Pass locale for the trigger label and a react-day-picker locale with dir: "rtl" in calendarProps for the calendar. Arrow keys follow the direction.

"use client"

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

import { DatePicker, DateRangePicker } from "@/components/ui/date-picker"

export function DatePickerRtl() {
  return (
    <div dir="rtl" className="flex flex-wrap justify-center gap-2">
      <DatePicker
        locale="ar"
        placeholder="اختر تاريخًا"
        title="اختر تاريخًا"
        clearLabel="مسح"
        clearable
        defaultValue={new Date(2026, 9, 2)}
        calendarProps={{ locale: arSA, dir: "rtl" }}
      />
      <DateRangePicker
        locale="ar"
        placeholder="اختر فترة"
        title="اختر فترة"
        calendarProps={{ locale: arSA, dir: "rtl" }}
      />
    </div>
  )
}
KeyAction
EnterSpaceOn the trigger, opens the picker and focuses the selected day, or today. On a day, selects it.
←→Moves to the previous or next day.
↑↓Moves to the same day in the previous or next week.
Page UpPage DownMoves to the previous or next month.
ShiftPage UpMoves to the previous year. Shift Page Down moves to the next.
HomeEndMoves to the start or end of the week.
EscCloses the picker and returns focus to the trigger.
  • The popover is labelled with title, and the bottom sheet shows it as a heading. Translate it along with placeholder.
  • The trigger has no visible label of its own. Pair it with a <label /> or pass aria-label.
  • Disabled days are skipped by the keyboard and announced as unavailable.

Both pickers share the props below and render a <Button /> trigger with a <Calendar /> in a popover or bottom sheet.

PropTypeDefault
value
Date | null–
defaultValue
Date | nullnull
onValueChange
(value: Date | null) => void–
placeholder
string"Pick a date"
titleAccessible name of the popover, heading of the sheet.
string"Select a date"
open
boolean–
defaultOpen
booleanfalse
onOpenChange
(open: boolean) => void–
localeFormats the trigger label with Intl.DateTimeFormat. Pass a react-day-picker locale in calendarProps to translate the calendar too.
string"en-US"
clearableAdds a Clear button below the calendar once there is a value.
booleanfalse
clearLabel
string"Clear"
nameRenders a hidden input so the value is submitted with its form.
string–
calendarPropsPassed to the calendar, except mode, selected, onSelect, required, numberOfMonths and autoFocus. Use it for disabled days, captionLayout, startMonth, endMonth, locale and dir.
CalendarProps–
variantThe trigger is a Button, and accepts its other props too.
Button variant"outline"
disabled
booleanfalse
classNameApplied to the trigger, which is w-60 by default.
string–
PropTypeDefault
value
{ from: Date; to?: Date } | null–
defaultValue
{ from: Date; to?: Date } | nullnull
onValueChangeCalled once the second day is picked.
(value: DateRange | null) => void–
placeholder
string"Pick a date range"
title
string"Select dates"
open
boolean–
defaultOpen
booleanfalse
onOpenChange
(open: boolean) => void–
localeFormats the trigger label with Intl.DateTimeFormat. Pass a react-day-picker locale in calendarProps to translate the calendar too.
string"en-US"
clearableAdds a Clear button below the calendar once there is a value.
booleanfalse
clearLabel
string"Clear"
nameRenders a hidden input so the value is submitted with its form.
string–
calendarPropsPassed to the calendar, except mode, selected, onSelect, required, numberOfMonths and autoFocus. Use it for disabled days, captionLayout, startMonth, endMonth, locale and dir.
CalendarProps–
variantThe trigger is a Button, and accepts its other props too.
Button variant"outline"
disabled
booleanfalse
classNameApplied to the trigger, which is w-60 by default.
string–
AttributeDescription
data-slot="date-picker-trigger"The trigger button.
data-emptyPresent on the trigger while there is no value. Shows the placeholder in muted text.
data-popup-openPresent on the trigger while the picker is open.
data-slot="date-picker-content"Wraps the calendar in the popover or sheet.
data-slot="date-picker-clear"The Clear button.