Skip to content

Instantly share code, notes, and snippets.

@mjbalcueva
Last active April 4, 2025 19:57
Show Gist options
  • Save mjbalcueva/1fbcb1be9ef68a82c14d778b686a04fa to your computer and use it in GitHub Desktop.
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>
)
}
@gmsetiawan
Copy link

Thank you.. awesome.

@ManrahulBajwa
Copy link

Still there is any issue the year and month dropdown not get selected according to the date value we are setting
image

My selected data is July 6th, 2016 but dropdowns are selected to like current date only.

@Maliksidk19
Copy link

Maliksidk19 commented Nov 22, 2024

@Mr-Vipi
Copy link

Mr-Vipi commented Nov 23, 2024

Still there is any issue the year and month dropdown not get selected according to the date value we are setting image

My selected data is July 6th, 2016 but dropdowns are selected to like current date only.

To solve your issue, in the Calendar component you have to use the defaultMonth prop with the value of the selected date

@owieth
Copy link

owieth commented Nov 24, 2024

focusing on the first dropdown leads to a thin line around:
image

to remove this, just add focus:ring-offset-0 to your <SelectTrigger />:

<SelectTrigger className="pr-1.5 focus:ring-0 focus:ring-offset-0">
    <SelectValue>{selected?.props?.children}</SelectValue>
</SelectTrigger>

@ManrahulBajwa
Copy link

Hi thanks for the information issue is fixed I missed defaultMonth prop with the value.
Thank you

@rohit-ks
Copy link

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

@MortyNiners
Copy link

Thank you!

@aynuayex
Copy link

@Maliksidk19 you mean this
Screenshot 2024-12-31 105522copy
here is mine, see 👁️ the difference.
Screenshot 2024-12-31 110336copy

@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 ✨

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