A calendar component that allows users to select a date or a range of dates.
1import * as React from "react";2import { Calendar } from "@/components/ui/calendar";3import { CalendarDate, type DateValue } from "@internationalized/date";4Installation
pnpm dlx sprawlify@latest add calendarnpx sprawlify@latest add calendaryarn sprawlify@latest add calendarbunx --bun sprawlify@latest add calendar
Install the following dependencies:
pnpm add @sprawlify/primitives @sprawlify/reactnpm install @sprawlify/primitives @sprawlify/reactyarn add @sprawlify/primitives @sprawlify/reactbun add @sprawlify/primitives @sprawlify/react
Add the following files to your project:
1"use client";23import * as React from "react";4import { cn } from "@/lib/utils";5import { Button } from "@/components/ui/button";6import { DatePicker as DatePickerPrimitive } from "@sprawlify/react/date-picker";7import { type DateValue } from "@internationalized/date";8import { ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon } from "lucide-react";910function CalendarViewHeader({11 buttonVariant = "ghost",12}: {13 buttonVariant?: React.ComponentProps<typeof Button>["variant"];14}) {15 return (16 <DatePickerPrimitive.ViewControl className="relative flex w-full items-center justify-between gap-1">17 <Button18 variant={buttonVariant}19 size="icon"20 className="size-(--cell-size) p-0 select-none aria-disabled:opacity-50"21 asChild22 >23 <DatePickerPrimitive.PrevTrigger>24 <ChevronLeftIcon className="size-4" />25 </DatePickerPrimitive.PrevTrigger>26 </Button>2728 <DatePickerPrimitive.ViewTrigger className="flex h-(--cell-size) items-center justify-center text-sm font-medium select-none">29 <DatePickerPrimitive.RangeText />30 </DatePickerPrimitive.ViewTrigger>3132 <Button33 variant={buttonVariant}34 size="icon"35 className="size-(--cell-size) p-0 select-none aria-disabled:opacity-50"36 asChild37 >38 <DatePickerPrimitive.NextTrigger>39 <ChevronRightIcon className="size-4" />40 </DatePickerPrimitive.NextTrigger>41 </Button>42 </DatePickerPrimitive.ViewControl>43 );44}4546function CalendarSelectHeader({47 buttonVariant = "ghost",48 className,49}: {50 buttonVariant?: React.ComponentProps<typeof Button>["variant"];51 className?: string;52}) {53 return (54 <DatePickerPrimitive.ViewControl55 className={cn("relative flex w-full items-center justify-between gap-1", className)}56 >57 <Button58 variant={buttonVariant}59 size="icon"60 className="size-(--cell-size) p-0 select-none aria-disabled:opacity-50"61 asChild62 >63 <DatePickerPrimitive.PrevTrigger>64 <ChevronLeftIcon className="size-4" />65 </DatePickerPrimitive.PrevTrigger>66 </Button>6768 <div className="flex items-center gap-1">69 <span className="relative">70 <DatePickerPrimitive.MonthSelect className="appearance-none rounded-md bg-transparent py-1 pr-6 pl-2 text-sm font-medium outline-none hover:bg-accent focus-visible:ring-[3px] focus-visible:ring-ring/50" />71 <ChevronDownIcon className="pointer-events-none absolute top-1/2 right-1 size-3.5 -translate-y-1/2 text-muted-foreground" />72 </span>73 <span className="relative">74 <DatePickerPrimitive.YearSelect className="appearance-none rounded-md bg-transparent py-1 pr-6 pl-2 text-sm font-medium outline-none hover:bg-accent focus-visible:ring-[3px] focus-visible:ring-ring/50" />75 <ChevronDownIcon className="pointer-events-none absolute top-1/2 right-1 size-3.5 -translate-y-1/2 text-muted-foreground" />76 </span>77 </div>7879 <Button80 variant={buttonVariant}81 size="icon"82 className="size-(--cell-size) p-0 select-none aria-disabled:opacity-50"83 asChild84 >85 <DatePickerPrimitive.NextTrigger>86 <ChevronRightIcon className="size-4" />87 </DatePickerPrimitive.NextTrigger>88 </Button>89 </DatePickerPrimitive.ViewControl>90 );91}9293function CalendarDayView({94 showOutsideDays = true,95 header,96 cell,97}: {98 showOutsideDays?: boolean;99 header?: React.ReactNode;100 cell?: (day: DateValue) => React.ReactNode;101}) {102 return (103 <DatePickerPrimitive.View view="day" className="flex flex-col gap-4">104 <DatePickerPrimitive.Context>105 {(api) => (106 <>107 {header ?? <CalendarSelectHeader />}108 <CalendarDayTable109 weeks={api.weeks}110 weekDays={api.weekDays}111 focusedMonth={api.focusedValue.month}112 showOutsideDays={showOutsideDays}113 cell={cell}114 />115 </>116 )}117 </DatePickerPrimitive.Context>118 </DatePickerPrimitive.View>119 );120}121122function CalendarMonthView({123 header,124 cell,125}: {126 header?: React.ReactNode;127 cell?: (month: { label: string; value: number }) => React.ReactNode;128}) {129 return (130 <DatePickerPrimitive.View view="month" className="flex flex-col gap-4">131 <DatePickerPrimitive.Context>132 {(api) => (133 <>134 {header ?? <CalendarSelectHeader />}135 <DatePickerPrimitive.Table className="w-full border-collapse">136 <DatePickerPrimitive.TableBody>137 {api.getMonthsGrid({ columns: 4, format: "short" }).map((months, rowIndex) => (138 <DatePickerPrimitive.TableRow key={rowIndex} className="mt-2 flex w-full">139 {months.map((month, colIndex) => (140 <DatePickerPrimitive.TableCell141 key={colIndex}142 value={month.value}143 className="flex-1 p-0 text-center"144 >145 <DatePickerPrimitive.TableCellTrigger asChild>146 <Button variant="ghost" className="w-full text-sm font-normal">147 {cell ? cell(month) : month.label}148 </Button>149 </DatePickerPrimitive.TableCellTrigger>150 </DatePickerPrimitive.TableCell>151 ))}152 </DatePickerPrimitive.TableRow>153 ))}154 </DatePickerPrimitive.TableBody>155 </DatePickerPrimitive.Table>156 </>157 )}158 </DatePickerPrimitive.Context>159 </DatePickerPrimitive.View>160 );161}162163function CalendarYearView({164 header,165 cell,166}: {167 header?: React.ReactNode;168 cell?: (year: { label: string; value: number }) => React.ReactNode;169}) {170 return (171 <DatePickerPrimitive.View view="year" className="flex flex-col gap-4">172 <DatePickerPrimitive.Context>173 {(api) => (174 <>175 {header ?? <CalendarSelectHeader />}176 <DatePickerPrimitive.Table className="w-full border-collapse">177 <DatePickerPrimitive.TableBody>178 {api.getYearsGrid({ columns: 4 }).map((years, rowIndex) => (179 <DatePickerPrimitive.TableRow key={rowIndex} className="mt-2 flex w-full">180 {years.map((year, colIndex) => (181 <DatePickerPrimitive.TableCell182 key={colIndex}183 value={year.value}184 className="flex-1 p-0 text-center"185 >186 <DatePickerPrimitive.TableCellTrigger asChild>187 <Button variant="ghost" className="w-full text-sm font-normal">188 {cell ? cell(year) : year.label}189 </Button>190 </DatePickerPrimitive.TableCellTrigger>191 </DatePickerPrimitive.TableCell>192 ))}193 </DatePickerPrimitive.TableRow>194 ))}195 </DatePickerPrimitive.TableBody>196 </DatePickerPrimitive.Table>197 </>198 )}199 </DatePickerPrimitive.Context>200 </DatePickerPrimitive.View>201 );202}203204function CalendarDayButton({205 children,206 className,207 ...props208}: React.ComponentProps<typeof DatePickerPrimitive.TableCellTrigger>) {209 return (210 <DatePickerPrimitive.TableCellTrigger211 className={cn(212 "relative isolate z-10 flex aspect-square size-auto w-full min-w-(--cell-size) items-center justify-center gap-1 rounded-(--cell-radius) border-0 text-sm leading-none font-normal",213 "hover:bg-accent hover:text-accent-foreground",214 "data-[selected]:bg-primary data-[selected]:text-primary-foreground",215 "data-[today]:bg-accent data-[today]:text-accent-foreground",216 "data-[outside-range]:text-muted-foreground/50",217 "data-[disabled]:text-muted-foreground data-[disabled]:opacity-50",218 "data-[unavailable]:text-muted-foreground data-[unavailable]:line-through data-[unavailable]:opacity-40",219 "data-[in-range]:rounded-none data-[in-range]:bg-accent data-[in-range]:text-accent-foreground",220 "data-[range-start]:rounded-l-(--cell-radius) data-[range-start]:bg-primary data-[range-start]:text-primary-foreground",221 "data-[range-end]:rounded-r-(--cell-radius) data-[range-end]:bg-primary data-[range-end]:text-primary-foreground",222 "focus-visible:relative focus-visible:z-10 focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1",223 className,224 )}225 {...props}226 >227 {children}228 </DatePickerPrimitive.TableCellTrigger>229 );230}231232function CalendarDayTable({233 weeks,234 weekDays,235 focusedMonth,236 showOutsideDays = true,237 visibleRange,238 cell,239}: {240 weeks: DateValue[][];241 weekDays: { short: string }[];242 focusedMonth: number;243 showOutsideDays?: boolean;244 visibleRange?: { start: DateValue; end: DateValue };245 cell?: (day: DateValue) => React.ReactNode;246}) {247 return (248 <DatePickerPrimitive.Table className="w-full border-collapse">249 <DatePickerPrimitive.TableHead>250 <DatePickerPrimitive.TableRow className="flex">251 {weekDays.map((weekDay, i) => (252 <DatePickerPrimitive.TableHeader253 key={i}254 className="flex-1 rounded-(--cell-radius) text-[0.8rem] font-normal text-muted-foreground select-none"255 >256 {weekDay.short}257 </DatePickerPrimitive.TableHeader>258 ))}259 </DatePickerPrimitive.TableRow>260 </DatePickerPrimitive.TableHead>261 <DatePickerPrimitive.TableBody>262 {weeks.map((week, weekIndex) => (263 <DatePickerPrimitive.TableRow key={weekIndex} className="mt-2 flex w-full">264 {week.map((day, dayIndex) => {265 const isOutside = day.month !== focusedMonth;266 if (!showOutsideDays && isOutside) {267 return <td key={dayIndex} className="flex-1 p-0" aria-hidden />;268 }269 return (270 <DatePickerPrimitive.TableCell271 key={dayIndex}272 value={day}273 visibleRange={visibleRange}274 className={cn(275 "group/day relative aspect-square h-full w-full flex-1 rounded-(--cell-radius) p-0 text-center select-none",276 "[&:last-child[data-selected]_div]:rounded-r-(--cell-radius)",277 "[&:first-child[data-selected]_div]:rounded-l-(--cell-radius)",278 )}279 >280 <CalendarDayButton>{cell ? cell(day) : day.day}</CalendarDayButton>281 </DatePickerPrimitive.TableCell>282 );283 })}284 </DatePickerPrimitive.TableRow>285 ))}286 </DatePickerPrimitive.TableBody>287 </DatePickerPrimitive.Table>288 );289}290291function CalendarDualMonthDayView({292 showOutsideDays = true,293 cell,294}: {295 showOutsideDays?: boolean;296 cell?: (day: DateValue) => React.ReactNode;297}) {298 return (299 <DatePickerPrimitive.View view="day" className="flex flex-col gap-4">300 <DatePickerPrimitive.Context>301 {(api) => {302 const offset = api.getOffset({ months: 1 });303 return (304 <div className="flex gap-4 flex-row">305 {/* First month */}306 <div className="flex flex-col gap-4">307 <div className="relative flex w-full items-center justify-between gap-1">308 <DatePickerPrimitive.PrevTrigger asChild>309 <Button310 variant="ghost"311 size="icon"312 className="size-(--cell-size) p-0 select-none aria-disabled:opacity-50"313 >314 <ChevronLeftIcon className="size-4" />315 </Button>316 </DatePickerPrimitive.PrevTrigger>317 <span className="text-sm font-medium select-none">318 {api.visibleRangeText.start}319 </span>320 <div className="size-(--cell-size)" />321 </div>322 <CalendarDayTable323 weeks={api.weeks}324 weekDays={api.weekDays}325 focusedMonth={api.focusedValue.month}326 showOutsideDays={showOutsideDays}327 cell={cell}328 />329 </div>330 {/* Second month */}331 <div className="flex flex-col gap-4">332 <div className="relative flex w-full items-center justify-between gap-1">333 <div className="size-(--cell-size)" />334 <span className="text-sm font-medium select-none">335 {api.visibleRangeText.end}336 </span>337 <DatePickerPrimitive.NextTrigger asChild>338 <Button339 variant="ghost"340 size="icon"341 className="size-(--cell-size) p-0 select-none aria-disabled:opacity-50"342 >343 <ChevronRightIcon className="size-4" />344 </Button>345 </DatePickerPrimitive.NextTrigger>346 </div>347 <CalendarDayTable348 weeks={offset.weeks}349 weekDays={api.weekDays}350 focusedMonth={offset.visibleRange.start.month}351 showOutsideDays={showOutsideDays}352 visibleRange={offset.visibleRange}353 cell={cell}354 />355 </div>356 </div>357 );358 }}359 </DatePickerPrimitive.Context>360 </DatePickerPrimitive.View>361 );362}363364function Calendar({365 className,366 showOutsideDays = true,367 children,368 numOfMonths,369 ...props370}: Omit<React.ComponentProps<typeof DatePickerPrimitive.Root>, "inline"> & {371 className?: string;372 showOutsideDays?: boolean;373 children?: React.ReactNode;374}) {375 const isDualMonth = numOfMonths && numOfMonths >= 2;376377 return (378 <DatePickerPrimitive.Root inline numOfMonths={numOfMonths} {...props}>379 <div380 data-slot="calendar"381 className={cn(382 "group/calendar w-fit bg-background p-2 [--cell-radius:var(--radius-md)] [--cell-size:--spacing(7)] in-data-[slot=card-content]:bg-transparent in-data-[slot=popover-content]:bg-transparent",383 className,384 )}385 >386 {children || (387 <>388 {isDualMonth ? (389 <CalendarDualMonthDayView showOutsideDays={showOutsideDays} />390 ) : (391 <CalendarDayView showOutsideDays={showOutsideDays} />392 )}393 <CalendarMonthView />394 <CalendarYearView />395 </>396 )}397 </div>398 </DatePickerPrimitive.Root>399 );400}401402const CalendarPresetTrigger = DatePickerPrimitive.PresetTrigger;403404export {405 Calendar,406 CalendarDayView,407 CalendarDualMonthDayView,408 CalendarDayTable,409 CalendarMonthView,410 CalendarYearView,411 CalendarViewHeader,412 CalendarSelectHeader,413 CalendarDayButton,414 CalendarPresetTrigger,415};416Update the import paths to match your project setup.
Usage
1import { Calendar } from "@/components/ui/calendar"1const [date, setDate] = React.useState<DateValue[]>([
2 new CalendarDate(2026, 3, 21),
3])
4
5return (
6 <Calendar
7 selectionMode="single"
8 value={date}
9 onValueChange={(details) =>
10 setDate(details.value)
11 }
12 className="rounded-lg border"
13 />
14)Examples
Basic
A basic calendar component. We used className="rounded-lg border" to style the calendar.
1import { Calendar } from "@/components/ui/calendar";23export default function Preview() {4 return <Calendar selectionMode="single" className="rounded-lg border" />;Calendar Booked Dates
1import * as React from "react";2import { Calendar } from "@/components/ui/calendar";3import { Card, CardContent } from "@/components/ui/card";4import { CalendarDate, type DateValue } from "@internationalized/date";Date And Time Picker
1import * as React from "react";2import { Calendar } from "@/components/ui/calendar";3import { Card, CardContent, CardFooter } from "@/components/ui/card";4import { Field, FieldGroup, FieldLabel } from "@/components/ui/field";Presets
1import { Button } from "@/components/ui/button";2import { Calendar, CalendarDayView, CalendarPresetTrigger } from "@/components/ui/calendar";3import { Card, CardContent, CardFooter } from "@/components/ui/card";4import { CalendarDate } from "@internationalized/date";Range Calendar
Use the selectionMode="range" prop to enable range selection.
1import * as React from "react";2import { Calendar } from "@/components/ui/calendar";3import { Card, CardContent } from "@/components/ui/card";4import { CalendarDate, type DateValue } from "@internationalized/date";Views
1import {2 Calendar,3 CalendarDayView,4 CalendarMonthView,On This Page
InstallationUsageExamplesBasicCalendar Booked DatesDate And Time PickerPresetsRange CalendarViewsGet PRO
Need premium blocks and templates? Upgrade to PRO and get access.