Skip to content

Instantly share code, notes, and snippets.

@Mr-Vipi
Last active January 17, 2026 21:16
Show Gist options
  • Select an option

  • Save Mr-Vipi/39a11119b8c37696650c2dd22021309d to your computer and use it in GitHub Desktop.

Select an option

Save Mr-Vipi/39a11119b8c37696650c2dd22021309d to your computer and use it in GitHub Desktop.
My custom shadcn calendar to include selection of month and year with react-day-picker-v.9
"use client"
import * as React from "react"
import {
ChevronDownIcon,
ChevronLeftIcon,
ChevronRightIcon,
} from "lucide-react"
import {
ChevronProps,
DayButtonProps,
DayPicker,
getDefaultClassNames,
RootProps,
WeekNumberProps,
} from "react-day-picker"
import { cn } from "@/lib/utils"
import { Button, buttonVariants } from "@/components/ui/button"
export type CalendarProps = React.ComponentPropsWithRef<typeof DayPicker> & {
buttonVariant?: React.ComponentProps<typeof Button>["variant"]
rootClassName?: string
monthsClassName?: string
monthClassName?: string
navClassName?: string
buttonPreviousClassName?: string
buttonNextClassName?: string
monthCaptionClassName?: string
dropdownsClassName?: string
dropdownRootClassName?: string
dropdownClassName?: string
captionLabelClassName?: string
monthGridClassName?: string
weekdaysClassName?: string
weekdayClassName?: string
weekClassName?: string
weekNumberHeaderClassName?: string
weekNumberClassName?: string
dayClassName?: string
rangeStartClassName?: string
rangeMiddleClassName?: string
rangeEndClassName?: string
todayClassName?: string
outsideClassName?: string
disabledClassName?: string
hiddenClassName?: string
footerClassName?: string
}
function Calendar({
className,
classNames,
showOutsideDays = true,
captionLayout = "label",
buttonVariant = "ghost",
components,
...props
}: CalendarProps) {
const defaultClassNames = getDefaultClassNames()
const buttonNavClassName = buttonVariants({
variant: buttonVariant,
className:
"size-[var(--cell-size)] aria-disabled:opacity-50 p-0 select-none",
})
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn(
"group/calendar bg-background p-3 [--cell-size:calc(.25rem*8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
className
)}
captionLayout={captionLayout}
classNames={{
root: cn("w-fit", defaultClassNames.root, props.rootClassName),
months: cn(
"relative flex flex-col gap-4 md:flex-row",
defaultClassNames.months,
props.monthsClassName
),
month: cn(
"flex flex-col w-full gap-4",
defaultClassNames.month,
props.monthClassName
),
nav: cn(
"flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between",
defaultClassNames.nav,
props.navClassName
),
button_previous: cn(
buttonNavClassName,
defaultClassNames.button_previous,
props.buttonPreviousClassName
),
button_next: cn(
buttonNavClassName,
defaultClassNames.button_next,
props.buttonNextClassName
),
month_caption: cn(
"flex items-center justify-center h-[var(--cell-size)] w-full px-[var(--cell-size)]",
defaultClassNames.month_caption,
props.monthCaptionClassName
),
dropdowns: cn(
"w-full flex items-center text-sm font-medium justify-center h-[var(--cell-size)] gap-1.5",
defaultClassNames.dropdowns,
props.dropdownsClassName
),
dropdown_root: cn(
"relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md",
defaultClassNames.dropdown_root,
props.dropdownRootClassName
),
dropdown: cn(
"absolute bg-popover inset-0 opacity-0",
defaultClassNames.dropdown,
props.dropdownClassName
),
caption_label: cn(
"select-none font-medium",
captionLayout === "label"
? "text-sm"
: "rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5",
defaultClassNames.caption_label,
props.captionLabelClassName
),
month_grid: cn(
"w-full border-collapse",
defaultClassNames.month_grid,
props.monthGridClassName
),
weekdays: cn(
"flex",
defaultClassNames.weekdays,
props.weekdaysClassName
),
weekday: cn(
"text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none",
defaultClassNames.weekday,
props.weekdayClassName
),
week: cn(
"flex w-full mt-2",
defaultClassNames.week,
props.weekClassName
),
week_number_header: cn(
"select-none w-[var(--cell-size)]",
defaultClassNames.week_number_header,
props.weekNumberHeaderClassName
),
week_number: cn(
"text-[0.8rem] select-none text-muted-foreground",
defaultClassNames.week_number,
props.weekNumberClassName
),
day: cn(
"relative h-full p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none",
"w-auto [tr:first-child_&]:w-full",
defaultClassNames.day,
props.dayClassName
),
range_start: cn(
"rounded-l-md bg-accent",
defaultClassNames.range_start,
props.rangeStartClassName
),
range_middle: cn(
"rounded-none",
defaultClassNames.range_middle,
props.rangeMiddleClassName
),
range_end: cn(
"rounded-r-md bg-accent",
defaultClassNames.range_end,
props.rangeEndClassName
),
today: cn(
"bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none",
defaultClassNames.today,
props.todayClassName
),
outside: cn(
"text-muted-foreground aria-selected:text-muted-foreground",
defaultClassNames.outside,
props.outsideClassName
),
disabled: cn(
"text-muted-foreground opacity-50",
defaultClassNames.disabled,
props.disabledClassName
),
hidden: cn(
"invisible",
defaultClassNames.hidden,
props.hiddenClassName
),
footer: cn(
"pt-3 text-sm",
defaultClassNames.footer,
props.footerClassName
),
...classNames,
}}
components={{
Root: CalendarRoot,
Chevron: CalendarChevron,
DayButton: CalendarDayButton,
WeekNumber: CalendarWeekNumber,
...components,
}}
{...props}
/>
)
}
function CalendarChevron({
className,
orientation,
...props
}: Readonly<ChevronProps>) {
if (orientation === "left") {
return <ChevronLeftIcon className={cn("size-4", className)} {...props} />
}
if (orientation === "right") {
return <ChevronRightIcon className={cn("size-4", className)} {...props} />
}
return <ChevronDownIcon className={cn("size-4", className)} {...props} />
}
function CalendarRoot({ className, rootRef, ...props }: RootProps) {
return (
<div
data-slot="calendar"
ref={rootRef}
className={cn(className)}
{...props}
/>
)
}
function CalendarWeekNumber({ children, ...props }: WeekNumberProps) {
return (
<td {...props}>
<div className="flex size-[var(--cell-size)] items-center justify-center text-center">
{children}
</div>
</td>
)
}
function CalendarDayButton({
className,
day,
modifiers,
...props
}: DayButtonProps) {
const defaultClassNames = getDefaultClassNames()
const ref = React.useRef<HTMLButtonElement>(null)
React.useEffect(() => {
if (modifiers.focused) ref.current?.focus()
}, [modifiers.focused])
return (
<Button
ref={ref}
variant="ghost"
size="icon"
data-day={day.date.toLocaleDateString()}
data-selected-single={
modifiers.selected &&
!modifiers.range_start &&
!modifiers.range_end &&
!modifiers.range_middle
}
data-range-start={modifiers.range_start}
data-range-end={modifiers.range_end}
data-range-middle={modifiers.range_middle}
className={cn(
"flex aspect-square size-auto w-full min-w-[var(--cell-size)] flex-col gap-1 font-normal leading-none data-[range-end=true]:rounded-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-start=true]:rounded-l-md data-[range-end=true]:bg-primary data-[range-middle=true]:bg-accent data-[range-start=true]:bg-primary data-[selected-single=true]:bg-primary data-[range-end=true]:text-primary-foreground data-[range-middle=true]:text-accent-foreground data-[range-start=true]:text-primary-foreground data-[selected-single=true]:text-primary-foreground group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-[3px] group-data-[focused=true]/day:ring-ring/50 dark:hover:text-accent-foreground [&>span]:text-xs [&>span]:opacity-70",
defaultClassNames.day,
className
)}
{...props}
/>
)
}
Calendar.displayName = "Calendar"
export { Calendar, CalendarDayButton }
"use client";
import {
addDays,
addMonths,
format,
isBefore,
isEqual,
isWithinInterval,
} from "date-fns";
import { Calendar as CalendarIcon } from "lucide-react";
import {
Dispatch,
memo,
SetStateAction,
useCallback,
useEffect,
useMemo,
useState,
} from "react";
import { DateRange } from "react-day-picker";
import { Button } from "@/components/ui/button";
import { Calendar } from "@/components/ui/calendar";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { cn } from "@/lib/utils";
// import { FieldType } from "./booking-form";
type DateRangePickerProps = Readonly<{
date?: DateRange;
setDate?: Dispatch<SetStateAction<DateRange | undefined>>;
// field?: FieldType;
focusedField?: string;
setFocusedField?: Dispatch<SetStateAction<string>>;
className?: string;
bookedDates?: DateRange[];
}>;
const fromMonth = new Date();
const toMonth = addMonths(new Date(), 6);
const DateRangePicker = ({
// date,
// setDate,
// field,
className,
bookedDates,
}: DateRangePickerProps) => {
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const [minimumDays, setMinimumDays] = useState<DateRange>({
from: undefined,
to: undefined,
});
const [date, setDate] = useState<DateRange | undefined>({
from: undefined,
to: undefined,
});
useEffect(() => {
if (!date) {
return;
}
if (date.from && date.to) {
setIsPopoverOpen(false);
setMinimumDays({
from: undefined,
to: undefined,
});
return;
}
if (date.from && !date.to) {
setMinimumDays({
from: addDays(date.from, 1),
to: addDays(date.from, 1),
});
} else {
setMinimumDays({
from: undefined,
to: undefined,
});
}
}, [date]);
function formatDateRange(from: Date, to: Date | undefined) {
const formattedFrom = format(from, "LLL dd, y");
return to ? `${formattedFrom} - ${format(to, "LLL dd, y")}` : formattedFrom;
}
const getDateText = useCallback(
(/*field: FieldType | undefined,*/ date: DateRange | undefined) => {
// if (field?.value) {
// const { from, to } = field.value;
// return from ? formatDateRange(from, to) : "Departure / Return";
// }
if (date) {
const { from, to } = date;
return from ? formatDateRange(from, to) : "Departure / Return";
}
return "Departure / Return";
},
[]
);
const checkIfBookingAvailable = useCallback(
({ from, to }: DateRange): boolean => {
let isAvailable = true;
if (bookedDates) {
bookedDates.forEach((bookedDate) => {
if (
!(
bookedDate.from &&
bookedDate.to &&
from &&
to &&
isWithinInterval(bookedDate.from, {
start: from,
end: to,
}) &&
isWithinInterval(bookedDate.to, { start: from, end: to })
)
) {
return;
}
isAvailable = false;
});
}
return isAvailable;
},
[bookedDates]
);
const updateDate = useCallback(
(day: Date): void => {
if (setDate) {
setDate((prev) => {
if (prev?.from && isEqual(prev.from, day)) {
return { from: undefined, to: undefined };
}
if (prev?.to) {
return { from: day, to: undefined };
} else if (prev?.from && isBefore(prev?.from, day)) {
return checkIfBookingAvailable({ from: prev?.from, to: day })
? { from: prev?.from, to: day }
: { from: day, to: undefined };
} else {
return { from: day, to: undefined };
}
});
}
// if (!field) {
// return;
// }
// const updateMinimumDaysAndResetToDate = () => {
// setMinimumDays({ from: addDays(day, 1), to: addDays(day, 1) });
// field.onChange({ from: day, to: undefined });
// };
// if (field.value.from && isEqual(field.value.from, day)) {
// field.onChange({ from: undefined, to: undefined });
// setMinimumDays({
// from: undefined,
// to: undefined,
// });
// return;
// }
// if (field.value.to) {
// updateMinimumDaysAndResetToDate();
// } else if (field.value.from && isBefore(field.value.from, day)) {
// if (checkIfBookingAvailable({ from: field.value.from, to: day })) {
// setIsPopoverOpen(false);
// field.onChange({ from: field.value.from, to: day });
// setMinimumDays({ from: undefined, to: undefined });
// } else {
// updateMinimumDaysAndResetToDate();
// }
// } else {
// updateMinimumDaysAndResetToDate();
// }
},
[setDate, /*field,*/ checkIfBookingAvailable]
);
const handleOpenChange = useCallback(
(value: boolean) => {
setIsPopoverOpen(value);
},
[setIsPopoverOpen]
);
const disabled = useMemo(
() => [
{
before: new Date(),
},
minimumDays,
...(bookedDates ?? []),
],
[minimumDays, bookedDates]
);
return (
<Popover open={isPopoverOpen} onOpenChange={handleOpenChange}>
<PopoverTrigger asChild>
<Button
variant="outline"
className={cn(
"w-full pl-3 text-left",
date?.from /*|| field?.value.from*/ ? "" : "text-muted-foreground",
className
)}
>
<span className="pt-1">{getDateText(/*field,*/ date)}</span>
<CalendarIcon className="ml-auto size-4 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="center">
<Calendar
autoFocus
mode="range"
defaultMonth={date?.from /*|| field?.value?.from*/}
selected={
date /*|| {
from: field?.value?.from,
to: field?.value?.to,
}*/
}
numberOfMonths={2}
showOutsideDays={false}
startMonth={fromMonth}
endMonth={toMonth}
onSelect={(_date, day) => updateDate(day)}
disabled={disabled}
/>
</PopoverContent>
</Popover>
);
};
const areEqual = (
prevProps: DateRangePickerProps,
nextProps: DateRangePickerProps
) => {
return (
prevProps?.date === nextProps.date &&
// prevProps?.field === nextProps.field &&
prevProps?.focusedField === nextProps.focusedField &&
prevProps?.bookedDates === nextProps.bookedDates &&
prevProps?.className === nextProps.className
);
};
export default memo(DateRangePicker, areEqual);
"use client";
import { format } from "date-fns";
import * as React from "react";
import { Button } from "@/components/ui/button";
import { Calendar } from "@/components/ui/calendar";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { cn } from "@/lib/utils";
import { CalendarIcon } from "lucide-react";
// Interval for the date picker
// const startDate = subYears(new Date(), 75);
// const endDate = subYears(new Date(), 23);
export default function DatePicker() {
const [date, setDate] = React.useState<Date>();
return (
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
className={cn(
"w-full pl-3 text-left",
date ? "md:text-sm" : "text-muted-foreground"
)}
>
{date ? format(date, "PPP") : <span>Pick a date</span>}
<CalendarIcon className="ml-auto size-4 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="center">
<Calendar
mode="single"
selected={date}
onSelect={setDate}
autoFocus
defaultMonth={date}
showOutsideDays={false}
captionLayout="dropdown"
// hideNavigation
// selectTriggerClassName="transition-colors duration-200 ease-in-out hover:border-primary focus:border-primary focus:shadow-around-primary focus:ring-0 focus:ring-offset-0"
// startMonth={startDate}
// endMonth={endDate}
// disabled={[
// { before: startDate },
// { after: endDate },
// ]}
// footer={DatePickerFooter}
/>
</PopoverContent>
</Popover>
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment