Skip to content

Instantly share code, notes, and snippets.

@mjbalcueva
Last active October 26, 2025 08:42
Show Gist options
  • Select an option

  • Save mjbalcueva/1fbcb1be9ef68a82c14d778b686a04fa to your computer and use it in GitHub Desktop.

Select an option

Save mjbalcueva/1fbcb1be9ef68a82c14d778b686a04fa to your computer and use it in GitHub Desktop.
shadcn ui calendar custom year and month dropdown
"use client"
import * as React from "react"
import { buttonVariants } from "@/components/ui/button"
import { ScrollArea } from "@/components/ui/scroll-area"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { cn } from "@/lib/utils"
import { ChevronLeft, ChevronRight } from "lucide-react"
import { DayPicker, DropdownProps } from "react-day-picker"
export type CalendarProps = React.ComponentProps<typeof DayPicker>
function Calendar({ className, classNames, showOutsideDays = true, ...props }: CalendarProps) {
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn("p-3", className)}
classNames={{
months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
month: "space-y-4",
caption: "flex justify-center pt-1 relative items-center",
caption_label: "text-sm font-medium",
caption_dropdowns: "flex justify-center gap-1",
nav: "space-x-1 flex items-center",
nav_button: cn(
buttonVariants({ variant: "outline" }),
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100"
),
nav_button_previous: "absolute left-1",
nav_button_next: "absolute right-1",
table: "w-full border-collapse space-y-1",
head_row: "flex",
head_cell: "text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]",
row: "flex w-full mt-2",
cell: "text-center text-sm p-0 relative [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20",
day: cn(buttonVariants({ variant: "ghost" }), "h-9 w-9 p-0 font-normal aria-selected:opacity-100"),
day_selected:
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
day_today: "bg-accent text-accent-foreground",
day_outside: "text-muted-foreground opacity-50",
day_disabled: "text-muted-foreground opacity-50",
day_range_middle: "aria-selected:bg-accent aria-selected:text-accent-foreground",
day_hidden: "invisible",
...classNames,
}}
components={{
Dropdown: ({ value, onChange, children, ...props }: DropdownProps) => {
const options = React.Children.toArray(children) as React.ReactElement<React.HTMLProps<HTMLOptionElement>>[]
const selected = options.find((child) => child.props.value === value)
const handleChange = (value: string) => {
const changeEvent = {
target: { value },
} as React.ChangeEvent<HTMLSelectElement>
onChange?.(changeEvent)
}
return (
<Select
value={value?.toString()}
onValueChange={(value) => {
handleChange(value)
}}
>
<SelectTrigger className="pr-1.5 focus:ring-0">
<SelectValue>{selected?.props?.children}</SelectValue>
</SelectTrigger>
<SelectContent position="popper">
<ScrollArea className="h-80">
{options.map((option, id: number) => (
<SelectItem key={`${option.props.value}-${id}`} value={option.props.value?.toString() ?? ""}>
{option.props.children}
</SelectItem>
))}
</ScrollArea>
</SelectContent>
</Select>
)
},
IconLeft: ({ ...props }) => <ChevronLeft className="h-4 w-4" />,
IconRight: ({ ...props }) => <ChevronRight className="h-4 w-4" />,
}}
{...props}
/>
)
}
Calendar.displayName = "Calendar"
export { Calendar }
/* add this snippet in your globals.css file */
.rdp-vhidden {
@apply hidden;
}
"use client"
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 { format } from "date-fns"
import { CalendarIcon } from "lucide-react"
export function SampleDatePicker() {
const [date, setDate] = React.useState<Date>()
return (
<Popover>
<PopoverTrigger asChild>
<Button
variant={"outline"}
className={cn("w-[240px] justify-start text-left font-normal", !date && "text-muted-foreground")}
>
<CalendarIcon className="mr-2 h-4 w-4" />
{date ? format(date, "PPP") : <span>Pick a date</span>}
</Button>
</PopoverTrigger>
<PopoverContent align="start" className=" w-auto p-0">
<Calendar
mode="single"
captionLayout="dropdown-buttons"
selected={date}
onSelect={setDate}
fromYear={1960}
toYear={2030}
/>
</PopoverContent>
</Popover>
)
}
@WarlockJa
Copy link

Is there a version of it, that works with react-day-picker v9?

I have managed to make it work in Next.js 15.1.3, with react 19, shadcn 2.1.8 and react-day-picker 9.5.0
Calendar component call

<PopoverContent className="w-auto p-0" align="start">
  <Calendar
    mode="single"
    captionLayout="dropdown"
    selected={field.value ?? new Date()}
    onSelect={field.onChange}
    endMonth={new Date()}
    disabled={{
      after: new Date(),
      before: new Date(1900, 0),
    }}
  />
</PopoverContent>

calendar.tsx

"use client";

import * as React from "react";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { DayPicker, DropdownProps } from "react-day-picker";

import { cn } from "@/lib/utils";
import { buttonVariants } from "@/components/ui/button";
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "./select";
import { ScrollArea } from "./scroll-area";

export type CalendarProps = React.ComponentProps<typeof DayPicker>;

function Calendar({
  className,
  classNames,
  showOutsideDays = true,
  ...props
}: CalendarProps) {
  return (
    <DayPicker
      showOutsideDays={showOutsideDays}
      className={cn("p-3", className)}
      classNames={{
        month: "space-y-4",
        months: "flex flex-col sm:flex-row space-y-4 sm:space-y-0 relative",
        month_caption: "flex justify-center pt-1 relative items-center",
        month_grid: "w-full border-collapse space-y-1",

        // TEST
        dropdowns: "flex justify-center gap-1",
        nav: "flex items-center justify-between absolute inset-x-0 top-2",
        button_previous: cn(
          buttonVariants({ variant: "outline" }),
          "h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100 z-10",
        ),
        button_next: cn(
          buttonVariants({ variant: "outline" }),
          "h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100 z-10",
        ),
        // TEST

        weeks: "w-full border-collapse space-y-",
        weekdays: "flex",
        weekday:
          "text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]",
        week: "flex w-full mt-2",
        day_button:
          "h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20",
        day: cn(
          buttonVariants({ variant: "ghost" }),
          "h-9 w-9 p-0 font-normal aria-selected:opacity-100",
        ),
        range_end: "day-range-end",
        selected:
          "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
        outside:
          "day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30",
        disabled: "text-muted-foreground opacity-50",
        range_middle:
          "aria-selected:bg-accent aria-selected:text-accent-foreground",
        hidden: "invisible",
        ...classNames,
      }}
      components={{
        Dropdown: ({ value, onChange, options }: DropdownProps) => {
          const selected = options?.find((child) => child.value === value);
          const handleChange = (value: string) => {
            const changeEvent = {
              target: { value },
            } as React.ChangeEvent<HTMLSelectElement>;
            onChange?.(changeEvent);
          };
          return (
            <Select
              value={value?.toString()}
              onValueChange={(value) => {
                handleChange(value);
              }}
            >
              <SelectTrigger className="pr-1.5 focus:ring-0">
                <SelectValue>{selected?.label}</SelectValue>
              </SelectTrigger>
              <SelectContent position="popper">
                <ScrollArea className="h-80">
                  {options?.map((option, id: number) => (
                    <SelectItem
                      key={`${option.value}-${id}`}
                      value={option.value?.toString() ?? ""}
                    >
                      {option.label}
                    </SelectItem>
                  ))}
                </ScrollArea>
              </SelectContent>
            </Select>
          );
        },
        Chevron: ({ ...props }) =>
          props.orientation === "left" ? (
            <ChevronLeft {...props} className="h-4 w-4" />
          ) : (
            <ChevronRight {...props} className="h-4 w-4" />
          ),
      }}
      {...props}
    />
  );
}
Calendar.displayName = "Calendar";

export { Calendar };

@VijayPonni
Copy link

A small change, instead of adding to the CSS file

.rdp-vhidden {
  @apply hidden;
}

Apply the Tailwind selector directly in the component declaration:

   <DayPicker
      showOutsideDays={showOutsideDays}
      className={cn("p-3", className)}
      classNames={{
        months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
        month: "space-y-4",
        //...
        vhidden: "vhidden hidden", // Add this line
        //..
      }}

Thank you very much

@Mr-Vipi
Copy link

Mr-Vipi commented Jan 21, 2025

Is there a version of it, that works with react-day-picker v9?

@rohit-ks

here you have my version of the calendar with react-day-picker v.9

shadcn calendar component with react-day-picer v.9+

@eliac7
Copy link

eliac7 commented Jan 22, 2025

Is there a version of it, that works with react-day-picker v9?

@rohit-ks

here you have my version of the calendar with react-day-picker v.9

shadcn calendar component with react-day-picer v.9+

Thanks a lot! Works perfectly.

@maxgfr
Copy link

maxgfr commented Feb 8, 2025

I can share you one of a component which works too : https://gist.github.com/maxgfr/94b00cfc2a8bb4031f36e52b0923b56d

@Maliksidk19
Copy link

Maliksidk19 commented Feb 14, 2025

Shadcn datetime picker is now updated to work with [email protected], react@19 and [email protected] without any issue

https://shadcn-datetime-picker.vercel.app/datetime-picker

@Mr-Vipi (I have used your react-day-picker v9 calendae component code and changed it a bit) Thanks ✨

@Maliksidk19
Copy link

you have to use the calendar component in the repository not the shadcn default one, try that and see

@arvind4403
Copy link

you have to use the calendar component in the repository not the shadcn default one, try that and see

i have component called calendar.tsx

in that i have this code

"use client";

import * as React from "react";
import { DayPicker, Dropdown as DropDownDayPicker } from "react-day-picker";

import { buttonVariants } from "@/components/ui/button";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { cn } from "@/lib/utils";

export type CalendarProps = React.ComponentProps & {
captionLabelClassName?: string;
dayClassName?: string;
dayButtonClassName?: string;
dropdownsClassName?: string;
footerClassName?: string;
monthClassName?: string;
monthCaptionClassName?: string;
monthGridClassName?: string;
monthsClassName?: string;
weekClassName?: string;
weekdayClassName?: string;
weekdaysClassName?: string;
rangeEndClassName?: string;
rangeMiddleClassName?: string;
rangeStartClassName?: string;
selectedClassName?: string;
disabledClassName?: string;
hiddenClassName?: string;
outsideClassName?: string;
todayClassName?: string;
selectTriggerClassName?: string;
};

function Calendar({
className,
classNames,
hideNavigation,
showOutsideDays = true,
components: customComponents,
...props
}: CalendarProps) {
const _monthsClassName = cn(
"relative flex flex-col gap-4 sm:flex-row",
props.monthsClassName,
);
const _monthCaptionClassName = cn(
"relative flex h-7 items-center justify-center",
props.monthCaptionClassName,
);
const _dropdownsClassName = cn(
"flex items-center justify-center gap-2 w-full",
hideNavigation ? "w-full" : "",
props.dropdownsClassName,
);
const _footerClassName = cn("pt-3 text-sm", props.footerClassName);
const _weekdaysClassName = cn("flex", props.weekdaysClassName);
const _weekdayClassName = cn(
"w-9 text-sm font-normal text-muted-foreground",
props.weekdayClassName,
);
const _captionLabelClassName = cn(
"truncate text-sm font-medium",
props.captionLabelClassName,
);

const _monthGridClassName = cn("mx-auto mt-4", props.monthGridClassName);
const _weekClassName = cn("mt-2 flex w-max items-start", props.weekClassName);
const _dayClassName = cn(
"flex size-9 flex-1 items-center justify-center p-0 text-sm",
props.dayClassName,
);
const _dayButtonClassName = cn(
buttonVariants({ variant: "ghost" }),
"size-9 rounded-md p-0 font-normal transition-none aria-selected:opacity-100",
props.dayButtonClassName,
);

const buttonRangeClassName =
"bg-accent [&>button]:bg-primary [&>button]:text-primary-foreground [&>button]:hover:bg-primary [&>button]:hover:text-primary-foreground";
const _rangeStartClassName = cn(
buttonRangeClassName,
"rounded-s-md",
props.rangeStartClassName,
);
const _rangeEndClassName = cn(
buttonRangeClassName,
"rounded-e-md",
props.rangeEndClassName,
);
const _rangeMiddleClassName = cn(
"bg-accent !text-foreground [&>button]:bg-transparent [&>button]:!text-foreground [&>button]:hover:bg-transparent [&>button]:hover:!text-foreground",
props.rangeMiddleClassName,
);
const _selectedClassName = cn(
"[&>button]:bg-primary [&>button]:text-primary-foreground [&>button]:hover:bg-primary [&>button]:hover:text-primary-foreground",
props.selectedClassName,
);
const _todayClassName = cn(
"[&>button]:bg-accent [&>button]:text-accent-foreground",
props.todayClassName,
);
const _outsideClassName = cn(
"text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30",
props.outsideClassName,
);
const _disabledClassName = cn(
"text-muted-foreground opacity-50",
props.disabledClassName,
);
const _hiddenClassName = cn("invisible flex-1", props.hiddenClassName);

const Dropdown = React.useCallback(
({
value,
onChange,
options,
}: React.ComponentProps) => {
const selected = options?.find((option) => option.value === value);
const handleChange = (value: string) => {
const changeEvent = {
target: { value },
} as React.ChangeEvent;
onChange?.(changeEvent);
};
return (
<Select
value={value?.toString()}
onValueChange={(value) => {
handleChange(value);
}}
>

{selected?.label}



{options?.map(({ value, label, disabled }, id) => (
<SelectItem
key={${value}-${id}}
value={value?.toString()}
disabled={disabled}
>
{label}

))}



);
},
[],
);

return (
<DayPicker
showOutsideDays={showOutsideDays}
hideNavigation={true} // Ensure navigation is hidden
className={cn("p-3", className)}
classNames={{
caption_label: _captionLabelClassName,
day: _dayClassName,
day_button: _dayButtonClassName,
dropdowns: _dropdownsClassName,
footer: _footerClassName,
month: props.monthClassName,
month_caption: _monthCaptionClassName,
month_grid: _monthGridClassName,
months: _monthsClassName,
week: _weekClassName,
weekday: _weekdayClassName,
weekdays: _weekdaysClassName,
range_end: _rangeEndClassName,
range_middle: _rangeMiddleClassName,
range_start: _rangeStartClassName,
selected: _selectedClassName,
disabled: _disabledClassName,
hidden: _hiddenClassName,
outside: _outsideClassName,
today: _todayClassName,
nav: "hidden", // This hides the navigation (chevrons)
...classNames,
}}
components={{
Dropdown,
...customComponents,
}}
{...props}
/>
);
}
Calendar.displayName = "Calendar";

export { Calendar };

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment