Skip to content

Instantly share code, notes, and snippets.

@mjbalcueva
Last active May 29, 2024 00:39
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>
)
}
@mjbalcueva
Copy link
Author

thanks @everyone for the positive feedback overall. If you guys are interested, I've made a custom password input component -> here

@raybagas7
Copy link

This is awesome, thanks a lot mark @mjbalcueva

@foxxxesky
Copy link

Thanks @mjbalcueva 🔥🔥

@brennerpablo
Copy link

brennerpablo commented Dec 21, 2023 via email

@chornthorn
Copy link

Thank you!

@RonaldErnst
Copy link

When I add captionLayout="dropdown" and fromYear and toYear to my Calendar component it always looks as if there is a double render/glitch when the calendar is opening. Do you perhaps know why?

Having the same issue. Adding dropwodn or dropdown-buttons causes a double render.

@Genji-kun
Copy link

Thank you, it helps me a lot!

@johnnysn
Copy link

Great job!

@GambetaClub
Copy link

Thank you very much! Looks great!

@mtnoronha
Copy link

mtnoronha commented Jan 30, 2024

@mjbalcueva man you're the best bro. Saved me a ton of time! Can't express in words how much grateful I am. God bless you.
Since you like helping others, I wanna share something I've done: a Combobox with an auto-complete feature.
If you want to check it out and share with the community, here it is:
shadcn-ui/ui#2577 (comment)

@liuhe2020
Copy link

@mjbalcueva man you're the best bro. Saved me a ton of time! Can't express in words how much grateful I am. God bless you. Since you like helping others, I wanna share something I've done: a Combobox with an auto-complete feature. If you want to check it out and share with the community, here it is: shadcn-ui/ui#2577 (comment)

Hi man, does your combobox work with multi-select like headless ui?

@sommeeeer
Copy link

awesome thanks

@rogerndutiye
Copy link

@mjbalcueva thank you , this is so good !

@MathisTimo
Copy link

thanks for sharing this

@coded58
Copy link

coded58 commented Mar 1, 2024

@mjbalcueva thank you for this. Has anyone used this in a modal?? Having some weird behaviors on the dropdowns

@arthurtsenin
Copy link

Using this component, after selecting current date selectedValue = undefined. To solve this issue add to calendar prop required according https://react-day-picker.js.org/basics/selecting-days

@nopitown
Copy link

nopitown commented Mar 15, 2024

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
        //..
      }}

@nopitown
Copy link

@jramiresbrito Try adding this classname caption_dropdowns: "flex justify-center gap-x-2" for fixing the dropdowns in a row. If you put the code in a live example, I can try to help with the popover issue.

@jramiresbrito
Copy link

@jramiresbrito Try adding this classname caption_dropdowns: "flex justify-center gap-x-2" for fixing the dropdowns in a row. If you put the code in a live example, I can try to help with the popover issue.

Thanks for replying. I went for an easier solution by just adding classes instead of modifying the component broadly

@KhatriFaiz
Copy link

Thank you!

@nadetastic
Copy link

Thank you for sharing this

@Raczan
Copy link

Raczan commented Mar 29, 2024

Thanks a lot

@aynuayex
Copy link

@mjbalcueva 👏 great work. but I'm always wondering why shadcn-ui does not merges this kind of changes quickly?
@liuhe2020

Hey man, do you know how I can close the modal when the user selects a date, instead of having to click outside again? onSelect={field.onChange} I suppose I need to change this but I am not sure how.

here is how you make the datepicker close after selection of date

 const [isCalendarOpen, setIsCalendarOpen] = useState(false);
  return (
    <Popover open={isCalendarOpen} onOpenChange={setIsCalendarOpen}>
      <PopoverTrigger asChild>
        <Button
          variant={"outline"}
          className={cn(
            "overflow-hidden   dark:text-white",
            !date && "text-muted-foreground",
            className
          )}
        >
          <CalendarIcon className="mr-2 h-4 w-4" />
          {date ? (
            <span>
              {window.innerWidth > 1024
                ? format(date, "PPP")
                : format(date, "d MMM")}
            </span>
          ) : (
            <span className="hidden sm:block">Pick a date</span>
          )}
          <ChevronsUpDown className="sm:ml-2 h-4 w-4 shrink-0 opacity-50 " />
        </Button>
      </PopoverTrigger>
      <PopoverContent className="w-auto p-0">
        <Calendar
          mode="single"
          selected={date}
          onSelect={(e) => {
            setDate(e);
            setIsCalendarOpen(false);
          }}
          initialFocus
          captionLayout="dropdown-buttons"
          fromYear={1990}
          toYear={2025}
        />
      </PopoverContent>
    </Popover>

@aynuayex
Copy link

aynuayex commented May 20, 2024

one logic i want my datepicker to have is after i have selected a date at some point and it closes.when i again clicked on it and it opens i want the date to point to the pervious date that i have selected already instead of the default initial state date?how do i implement this any ideas would be apperciated.
here is my implmentation:

export default function MainContent() {
  const [date, setDate] = useState<Date | undefined>(new Date());
 
  return (
    <div className="w-full mt-24 absolute pr-1 sm:pr-4  ">
        <DatePicker
          className="w-[100px] sm:w-1/4 lg:w-48"
          date={date}
          setDate={setDate}
        />
    </div>
  );
}

type DatePickerProps = {
  className: string;
  date: Date | undefined;
  setDate: React.Dispatch<React.SetStateAction<Date | undefined>>;
};

const DatePicker = ({ className, date, setDate }: DatePickerProps) => {
  const [isCalendarOpen, setIsCalendarOpen] = useState(false);
  return (
    <Popover open={isCalendarOpen} onOpenChange={setIsCalendarOpen}>
      <PopoverTrigger asChild>
        <Button
          variant={"outline"}
          className={cn(
            "overflow-hidden   dark:text-white",
            !date && "text-muted-foreground",
            className
          )}
        >
          <CalendarIcon className="mr-2 h-4 w-4" />
          {date ? (
            <span>
              {window.innerWidth > 1024
                ? format(date, "PPP")
                : format(date, "d MMM")}
            </span>
          ) : (
            <span className="hidden sm:block">Pick a date</span>
          )}
          <ChevronsUpDown className="sm:ml-2 h-4 w-4 shrink-0 opacity-50 " />
        </Button>
      </PopoverTrigger>
      <PopoverContent className="w-auto p-0">
        <Calendar
          mode="single"
          selected={date}
          onSelect={(e) => {
            setDate(e);
            setIsCalendarOpen(false);
          }}
          initialFocus
          captionLayout="dropdown-buttons"
          fromYear={1990}
          toYear={2025}
        />
      </PopoverContent>
    </Popover>
  );
};

@HamidDev22
Copy link

Thank you man!

@alexwhb
Copy link

alexwhb commented May 27, 2024

This saved me big time!!! Thank you so much!

@aynuayex
Copy link

aynuayex commented May 28, 2024

one logic i want my datepicker to have is after i have selected a date at some point and it closes.when i again clicked on it and it opens i want the date to point to the pervious date that i have selected already instead of the default initial state date?how do i implement this any ideas would be apperciated.

here is how i have come up to implement it by following from the docs and it works as expected.

export default function MainContent() {
  const [date, setDate] = useState<Date | undefined>(new Date());
 
  return (
    <div className="w-full mt-24 absolute pr-1 sm:pr-4  ">
        <DatePicker
          className="w-[100px] sm:w-1/4 lg:w-48"
          date={date}
          setDate={setDate}
        />
    </div>
  );
}

type DatePickerProps = {
  className: string;
  date: Date | undefined;
  setDate: React.Dispatch<React.SetStateAction<Date | undefined>>;
};

const DatePicker = ({ className, date, setDate }: DatePickerProps) => {
  const [ month, setMonth ] = useState<Date | undefined>(new Date());
  const [isCalendarOpen, setIsCalendarOpen] = useState(false);
  return (
    <Popover open={isCalendarOpen} onOpenChange={setIsCalendarOpen}>
      <PopoverTrigger asChild>
        <Button
          variant={"outline"}
          className={cn(
            "overflow-hidden   dark:text-white",
            !date && "text-muted-foreground",
            className
          )}
        >
          <CalendarIcon className="mr-2 h-4 w-4" />
          {date ? (
            <span>
              {window.innerWidth > 1024
                ? format(updateDatePart(month as Date,date), "PPP")
                : format(updateDatePart(month as Date,date), "d MMM")}
            </span>
          ) : (
            <span className="hidden sm:block">Pick a date</span>
          )}
          <ChevronsUpDown className="sm:ml-2 h-4 w-4 shrink-0 opacity-50 " />
        </Button>
      </PopoverTrigger>
      <PopoverContent className="w-auto p-0">
        <Calendar
          mode="single"
          month={month}
          onMonthChange={(month) => setMonth(month)}
          selected={date}
          onSelect={(e) => {
            setDate(e);
            setIsCalendarOpen(false);
          }}
          initialFocus
          captionLayout="dropdown-buttons"
          fromYear={1990}
          toYear={2025}
        />
      </PopoverContent>
    </Popover>
  );
};

lib/utils.tsx

import { format, parse, setDate } from "date-fns"

export const updateDatePart = (month: Date, newDay: Date): Date => {
  return setDate(month, newDay.getDate());
};

@ErhanArda
Copy link

i have a problem here is that despite selecting a date (e.g., 11/06/2025), the calendar opens to May 2024 instead of June 2025. The calendar is not properly focusing on the selected date when reopened.
image

@ErhanArda
Copy link

ErhanArda commented May 28, 2024

i have a problem here is that despite selecting a date (e.g., 11/06/2025), the calendar opens to May 2024 instead of June 2025. The calendar is not properly focusing on the selected date when reopened.

image

defaultMonth={value} solved my problem 💯

@aynuayex
Copy link

aynuayex commented May 28, 2024

@ErhanArda no setting defaultMonth={value} does not allow you to programatically control the month it just sets a default month which means every time it opens it is set to that default month read here, but if you want to programatically control the month, you have to set it like i have put above.you can read here more.
if it does not work for you please check your code so that it matches mine, or create a sandbox and i will check it.it works fine for me and hell yea it should for you to.

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