Primitives/Calendar
Calendar
react-day-picker calendar with single and range selection, month/year dropdowns.
- Version
- 1.0.1
- Tier
- Primitive
- Updates taken
- Minor and patch
- Files
- 1
Preview
September 2026
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
Install
In a project already set up on the contract, which is what registers the @ja3dan prefix. If yours isn't, set it up first.
bunx shadcn@latest add @ja3dan/calendar Also installs react-day-picker lucide-react if the project doesn't have them.
Related items
Installs alongside it
Used by
Source
What lands in your project, read from the published item rather than the working tree.
"use client";import * as React from "react";import { DayPicker, getDefaultClassNames, type CustomComponents, type DayButton, type Locale,} from "react-day-picker";import { ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon } from "lucide-react";import { Button, buttonVariants } from "@/registry/groundwork/ui/button";import { cn } from "@/lib/utils";function Calendar({ className, classNames, showOutsideDays = true, captionLayout = "label", buttonVariant = "ghost", locale, formatters, components, ...props}: React.ComponentProps<typeof DayPicker> & { buttonVariant?: React.ComponentProps<typeof Button>["variant"];}) { const defaultClassNames = getDefaultClassNames(); // Stable component references: inline component definitions here would remount the // whole grid on every parent render (e.g. picking the first day of a range). const mergedComponents = React.useMemo(() => ({ ...BASE_COMPONENTS, ...components }), [components]); return ( <CalendarLocaleContext.Provider value={locale}> <DayPicker showOutsideDays={showOutsideDays} className={cn( "group/calendar bg-card p-2 [--cell-radius:var(--radius-md)] [--cell-size:--spacing(8)] in-data-[slot=popover-content]:bg-transparent", className, )} captionLayout={captionLayout} locale={locale} formatters={{ formatMonthDropdown: (date) => date.toLocaleString(locale?.code, { month: "short" }), ...formatters, }} classNames={{ root: cn("w-fit", defaultClassNames.root), months: cn( "relative flex flex-col gap-4 md:flex-row", defaultClassNames.months, ), month: cn("flex w-full flex-col gap-4", defaultClassNames.month), nav: cn( "absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1", defaultClassNames.nav, ), button_previous: cn( buttonVariants({ variant: buttonVariant }), "size-(--cell-size) p-0 select-none aria-disabled:opacity-50", defaultClassNames.button_previous, ), button_next: cn( buttonVariants({ variant: buttonVariant }), "size-(--cell-size) p-0 select-none aria-disabled:opacity-50", defaultClassNames.button_next, ), month_caption: cn( "flex h-(--cell-size) w-full items-center justify-center px-(--cell-size)", defaultClassNames.month_caption, ), dropdowns: cn( "flex h-(--cell-size) w-full items-center justify-center gap-2 text-sm font-medium text-foreground", defaultClassNames.dropdowns, ), dropdown_root: cn( "relative rounded-(--cell-radius) px-2 transition-colors duration-150 ease-out has-focus-visible:ring-1 has-focus-visible:ring-ring hover:bg-accent", defaultClassNames.dropdown_root, ), dropdown: cn( "absolute inset-0 bg-popover opacity-0", defaultClassNames.dropdown, ), caption_label: cn( "font-medium text-foreground select-none", captionLayout === "label" ? "text-sm" : "flex items-center gap-1 rounded-(--cell-radius) text-sm [&>svg]:size-3 [&>svg]:text-muted-foreground", defaultClassNames.caption_label, ), month_grid: cn("w-full border-collapse", defaultClassNames.month_grid), weekdays: cn("flex", defaultClassNames.weekdays), weekday: cn( "flex-1 rounded-(--cell-radius) text-xs font-normal text-subtle-foreground select-none", defaultClassNames.weekday, ), week: cn("mt-2 flex w-full", defaultClassNames.week), week_number_header: cn( "w-(--cell-size) select-none", defaultClassNames.week_number_header, ), week_number: cn( "text-xs text-subtle-foreground select-none", defaultClassNames.week_number, ), day: cn( "group/day relative aspect-square h-full w-full rounded-(--cell-radius) p-0 text-center select-none [&:last-child[data-selected=true]_button]:rounded-r-(--cell-radius)", props.showWeekNumber ? "[&:nth-child(2)[data-selected=true]_button]:rounded-l-(--cell-radius)" : "[&:first-child[data-selected=true]_button]:rounded-l-(--cell-radius)", defaultClassNames.day, ), range_start: cn( "relative isolate z-0 rounded-l-(--cell-radius) bg-primary/15 after:absolute after:inset-y-0 after:right-0 after:w-4 after:bg-primary/15", defaultClassNames.range_start, ), range_middle: cn("rounded-none", defaultClassNames.range_middle), range_end: cn( "relative isolate z-0 rounded-r-(--cell-radius) bg-primary/15 after:absolute after:inset-y-0 after:left-0 after:w-4 after:bg-primary/15", defaultClassNames.range_end, ), today: cn( "rounded-(--cell-radius) bg-muted text-foreground data-[selected=true]:rounded-none", defaultClassNames.today, ), outside: cn( "text-subtle-foreground aria-selected:text-subtle-foreground", defaultClassNames.outside, ), disabled: cn("text-subtle-foreground opacity-50", defaultClassNames.disabled), hidden: cn("invisible", defaultClassNames.hidden), ...classNames, }} components={mergedComponents} {...props} /> </CalendarLocaleContext.Provider> );}const CalendarLocaleContext = React.createContext<Partial<Locale> | undefined>(undefined);function CalendarRoot({ className, rootRef, ...props }: React.ComponentProps<CustomComponents["Root"]>) { return <div data-slot="calendar" ref={rootRef} className={cn(className)} {...props} />;}function CalendarChevron({ className, orientation, ...props }: React.ComponentProps<CustomComponents["Chevron"]>) { 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 CalendarWeekNumber({ children, ...props }: React.ComponentProps<CustomComponents["WeekNumber"]>) { return ( <td {...props}> <div className="flex size-(--cell-size) items-center justify-center text-center">{children}</div> </td> );}function CalendarDayButton({ className, day, modifiers, locale, ...props}: React.ComponentProps<typeof DayButton> & { locale?: Partial<Locale> }) { const contextLocale = React.useContext(CalendarLocaleContext); locale ??= contextLocale; 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(locale?.code)} 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( "relative isolate z-10 flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 border-0 leading-none font-normal text-foreground active:not-disabled:scale-100 group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-1 group-data-[focused=true]/day:ring-ring data-[range-end=true]:rounded-(--cell-radius) data-[range-end=true]:rounded-r-(--cell-radius) data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground data-[range-middle=true]:rounded-none data-[range-middle=true]:bg-primary/15 data-[range-middle=true]:text-foreground data-[range-start=true]:rounded-(--cell-radius) data-[range-start=true]:rounded-l-(--cell-radius) data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[selected-single=true]:hover:bg-primary/90 [&>span]:text-xs [&>span]:opacity-70", defaultClassNames.day, className, )} {...props} /> );}const BASE_COMPONENTS: Partial<CustomComponents> = { Root: CalendarRoot, Chevron: CalendarChevron, DayButton: CalendarDayButton, WeekNumber: CalendarWeekNumber,};export { Calendar, CalendarDayButton };