HextaUI

Calendar

単一、範囲、複数選択に対応する日付グリッドです。月のスライド、範囲のプレビュー、タッチしやすいサイズの日付セルを備えています。

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

コンポーネント、HextaUIのテーマトークン、依存するHextaUIコンポーネントを追加します。

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

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

Calendar は react-day-picker の <DayPicker /> をラップしているため、DayPicker のすべての props が daypicker.dev に記載のとおりに動作します。HextaUI は、スタイリング、月の切り替えアニメーション、範囲のプレビュー、ハイドレーションに安全な today を追加します。

範囲

mode="range" では、最初のクリックの後に日付へホバーまたはフォーカスすると、次のクリックで選択される範囲がプレビューされます。numberOfMonths は月を横に並べて表示し、狭い画面では縦に積みます。

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

範囲の制限

min と max は範囲の日数を制限します。excludeDisabled を指定すると、無効な日を含む範囲は、含めずに最初からやり直しになります。

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

複数

mode="multiple" は個々の日付を切り替えます。max は選択できる数の上限を設定します。

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

月と年のドロップダウン

captionLayout="dropdown" はキャプションをネイティブの select に置き換えるため、スマートフォンでは標準のピッカーが使われます。年の範囲を限定するには startMonth と endMonth を設定します。

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

範囲を限定

移動は startMonth と endMonth で止まり、disabled はその範囲外の日を無効にします。useToday() は、サーバーレンダリング中も安全に使える今日の日付を返します。

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

制御された month

month と onMonthChange を渡すと、表示中の月を制御できます。ジャンプは移動の向きにスライドし、高さは 5 行と 6 行の週の間でイージングします。

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

週番号

showWeekNumber は週の列を追加します。ISOWeek は月曜始まりの ISO 週番号を使います。showOutsideDays={false} は他の月の日付を非表示にします。

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

today を渡すと、ハイライトする日を固定できます。テストや別のタイムゾーンに使えます。animate={false} は月の切り替えアニメーションを無効にします。

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

シートの中

シート、ポップオーバー、ダイアログの内側では、カレンダーは独自の背景をなくし、面に溶け込みます。

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

右から左

react-day-picker/locale の locale と dir="rtl" を渡します。矢印、ナビゲーション、スライドの向きがすべて反転します。dir="rtl" の DirectionProvider の内側では、方向が自動的に取得されます。

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

日付にフォーカスしてから、これらのキーを使います。表示中の月を越えると、月が切り替わります。

キーアクション
←→前日または翌日。右から左のレイアウトでは逆になります。
↑↓前週または翌週の同じ曜日。
Shift←→前月または翌月。
Shift↑↓前年または翌年。
Page UpPage Down前月または翌月。
ShiftPage UpPage Down前年または翌年。
Home週の最初の日。
End週の最後の日。
EnterSpaceフォーカス中の日を選択します。
  • 月はグリッドです。各日付は完全な日付ラベルを持つボタンで、選択された日付には aria-selected が設定されます。
  • キーボードによる月の切り替えは、スライドをスキップしてフェードのみになるため、動くグリッドの下でフォーカスが移動することはありません。
  • モーションの低減が有効な場合、月の切り替えはフェードになり、高さの変化は即座に行われます。
  • タッチスクリーンでは、日付セルが 44px に拡大します。

<DayPicker /> のすべての props を受け付けます。以下のデフォルト値は、DayPicker のものと異なるか、HextaUI が追加したものです。

プロパティ型デフォルト
modemode がない場合、日付は選択できません。
"single" | "multiple" | "range"–
selectedmode に合わせます。
Date | Date[] | DateRange–
onSelect
(selected, triggerDate, modifiers, event) => void–
required最後の選択を解除できないようにします。
boolean–
min範囲の最小日数、または multiple モードでの最小選択数。
number–
max範囲の最大日数、または multiple モードでの最大選択数。
number–
excludeDisabledrange モード。
boolean–
disabled
Matcher | Matcher[]–
month制御された month。
Date–
defaultMonth
Date–
onMonthChange
(month: Date) => void–
startMonth
Date–
endMonth
Date–
numberOfMonths
number1
captionLayout
"label" | "dropdown" | "dropdown-months" | "dropdown-years""label"
navLayoutHextaUI のデフォルト。矢印はキャプションの両側に配置されます。
"around" | "after""around"
showOutsideDaysHextaUI のデフォルト。
booleantrue
animate月のスライドと高さのトランジション。HextaUI のデフォルト。
booleantrue
buttonVariant前へ・次へボタンの variant。
Button variant"ghost"
showWeekNumber
booleanfalse
ISOWeek
booleanfalse
weekStartsOn
0 | 1 | 2 | 3 | 4 | 5 | 6–
fixedWeeks
booleanfalse
todayデフォルトはクライアントの今日で、日付変更時やハイドレーションをまたいで同期されます。
Date–
timeZone
string–
locale
Partial<DayPickerLocale>–
dir
"ltr" | "rtl"–
footer
ReactNode–
属性説明
data-slot="calendar"CSS でカレンダーのルートを指定します。
--cell-size日付セルのサイズ。36px で、タッチスクリーンでは 44px です。
--cell-radius日付セルとボタンの角丸の半径。
data-slot="calendar-day"日付セル。data-selected、data-disabled、data-outside、data-today、data-hidden、data-focused を持ちます。
data-preview日付セルに付与されます: ホバー中の範囲プレビューの開始、中間、終了。
data-range-middle選択された範囲内の日付セルに付与されます。

各日付の内側のボタン。独自のものを components={{ DayButton }} に渡し、スタイルを維持するにはこのコンポーネントを再利用します。

属性説明
data-slot="calendar-day-button"CSS で日付ボタンを指定します。
data-day2026-10-03 のような ISO 形式の日付。
data-today今日に付与されます。範囲外の日は除きます。
data-selected-single範囲外で選択された日。
data-range-start範囲の最初の日。
data-range-middle範囲内の日。
data-range-end範囲の最後の日。

クライアントでは今日を Date で、サーバーレンダリング中は undefined を返すため、これを使って作った境界値がハイドレーションの不一致を起こすことはありません。日付が変わる時刻に更新されます。useToday のガイドを参照してください。