A date grid for one day, several days or a range, with month and year selects, week numbers, disabled rules and range-end styling, on React DayPicker.
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
"use client";
import * as React from "react";
import type { DateRange } from "react-day-picker";
import { Calendar } from "@/components/ballmac/calendar";
export default function CalendarDemo() {
const [range, setRange] = React.useState<DateRange | undefined>({
from: new Date(2026, 8, 8),
to: new Date(2026, 8, 15),
});
const fmt = new Intl.DateTimeFormat("en-US", { month: "short", day: "numeric" });
const nights = range?.from && range.to ? Math.round((range.to.getTime() - range.from.getTime()) / 86400000) : 0;
return (
<div className="w-fit max-w-full rounded-xl border bg-card p-3 shadow-sm">
<Calendar
mode="range"
selected={range}
onSelect={setRange}
defaultMonth={new Date(2026, 8)}
disabled={{ before: new Date(2026, 8, 1) }}
footer={
range?.from && range.to
? `${fmt.format(range.from)} – ${fmt.format(range.to)} · ${nights} nights`
: "Pick a check-in and check-out date."
}
/>
</div>
);
}Installation
$ pnpm dlx shadcn@latest add @ballmac/calendarInstall the dependencies.
$ pnpm add react-day-picker@^9 lucide-reactAdd the Ballmac items it builds on.
$ pnpm dlx shadcn@latest add @ballmac/directionCopy the source into your project.
components/ballmac/calendar.tsx// Ballmac UI: Calendar. https://ui.ballmac.com/components/calendar // Based on shadcn/ui Calendar (MIT, Copyright (c) 2023 shadcn) on React DayPicker (MIT, Copyright (c) Giampaolo Bellavite), restyled with a token-driven day grid, rounded range ends, styled month and year selects, and a footer slot. "use client"; import * as React from "react"; import { ChevronDown, ChevronLeft, ChevronRight } from "lucide-react"; import { DayPicker } from "react-day-picker"; import { useDirection } from "@/lib/ballmac/direction"; import { cn } from "@/lib/utils"; type CalendarProps = React.ComponentProps<typeof DayPicker> & { /** Draw a border, padding and background around the calendar. Turn off inside a popover or card. */ bordered?: boolean; }; /** * A date grid for picking one day, several days or a range (`mode="single" | "multiple" | "range"`). * All React DayPicker props work: `selected`, `onSelect`, `disabled`, `fromDate`/`toDate`, `numberOfMonths`, * `captionLayout="dropdown"` for month and year selects, `showWeekNumber`, and `locale`. * Arrow keys move between days, PageUp/PageDown change month, Home/End jump within the week. */ function Calendar({ className, classNames, bordered = false, showOutsideDays = true, captionLayout = "label", components, ...props }: CalendarProps) { const dir = useDirection(props.dir === "rtl" || props.dir === "ltr" ? props.dir : undefined); return ( <DayPicker dir={dir} data-slot="calendar" showOutsideDays={showOutsideDays} captionLayout={captionLayout} className={cn( "group/calendar w-fit select-none [--cell-size:2.25rem]", bordered && "rounded-xl border bg-card p-3 text-card-foreground shadow-xs", className, )} classNames={{ root: "relative", months: "relative flex flex-col gap-4 sm:flex-row", month: "flex w-full flex-col gap-3", nav: "absolute inset-x-0 top-0 z-10 flex h-(--cell-size) items-center justify-between", button_previous: navButton, button_next: navButton, month_caption: "flex h-(--cell-size) items-center justify-center px-(--cell-size)", caption_label: cn( "text-sm font-semibold", captionLayout !== "label" && "flex h-8 items-center gap-1 rounded-md pe-1 ps-2 text-sm font-medium [&>svg]:size-3.5 [&>svg]:text-muted-foreground", ), dropdowns: "flex h-(--cell-size) items-center justify-center gap-1.5 text-sm font-medium", dropdown_root: "relative rounded-md border border-input shadow-xs has-focus-visible:border-ring has-focus-visible:ring-[3px] has-focus-visible:ring-ring/50", dropdown: "absolute inset-0 cursor-pointer opacity-0", months_dropdown: "", years_dropdown: "", month_grid: "w-full border-collapse", weekdays: "flex", weekday: "flex-1 pb-1 text-center text-[0.75rem] font-medium text-muted-foreground", week: "mt-1 flex w-full", week_number_header: "w-(--cell-size)", week_number: "w-(--cell-size) text-center text-xs text-muted-foreground", day: dayCell, day_button: dayButton, range_start: "rounded-s-md bg-accent [&>button]:rounded-md", range_middle: "rounded-none bg-accent [&>button]:!bg-transparent [&>button]:!text-accent-foreground [&>button]:hover:!bg-foreground/10", range_end: "rounded-e-md bg-accent [&>button]:rounded-md", selected: "[&>button]:bg-primary [&>button]:text-primary-foreground [&>button]:hover:bg-primary", today: "[&>button]:font-semibold [&:not([aria-selected=true])>button:not([data-range-middle])]:bg-accent [&:not([aria-selected=true])>button]:after:absolute [&:not([aria-selected=true])>button]:after:bottom-1 [&:not([aria-selected=true])>button]:after:left-1/2 [&:not([aria-selected=true])>button]:after:size-1 [&:not([aria-selected=true])>button]:after:-translate-x-1/2 [&:not([aria-selected=true])>button]:after:rounded-full [&:not([aria-selected=true])>button]:after:bg-primary", outside: "text-muted-foreground aria-selected:text-muted-foreground", disabled: "text-muted-foreground opacity-40 [&>button]:pointer-events-none", hidden: "invisible", footer: "mt-2 text-sm text-muted-foreground", ...classNames, }} components={{ Chevron: ({ orientation, className: c, ...rest }) => { const Icon = orientation === "left" ? ChevronLeft : orientation === "right" ? ChevronRight : ChevronDown; return <Icon aria-hidden="true" className={cn("size-4", c)} {...(rest as object)} />; }, ...components, }} {...props} /> ); } const navButton = "inline-flex size-(--cell-size) items-center justify-center rounded-md text-muted-foreground outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-disabled:pointer-events-none aria-disabled:opacity-40"; const dayCell = "group/day relative aspect-square h-(--cell-size) w-(--cell-size) flex-1 p-0 text-center text-sm"; const dayButton = "relative flex size-full items-center justify-center rounded-md text-sm font-normal tabular-nums outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:z-10 focus-visible:ring-[3px] focus-visible:ring-ring/50 motion-reduce:transition-none"; export { Calendar, type CalendarProps };Update the import paths to match your project setup.
Usage
import { Calendar } from "@/components/ballmac/calendar"The full example is in the Code tab above.
Examples
Dropdowns and multiple
| Su | Mo | Tu | We | Th | Fr | Sa | |
|---|---|---|---|---|---|---|---|
| 36 | |||||||
| 37 | |||||||
| 38 | |||||||
| 39 | |||||||
| 40 |
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
"use client";
import * as React from "react";
import { Calendar } from "@/components/ballmac/calendar";
export default function CalendarStates() {
const [days, setDays] = React.useState<Date[] | undefined>([new Date(2026, 8, 3), new Date(2026, 8, 17), new Date(2026, 8, 24)]);
const [day, setDay] = React.useState<Date | undefined>(new Date(2026, 8, 21));
return (
<div className="grid w-full max-w-sm gap-4">
<div className="w-fit max-w-full rounded-xl border bg-card p-3">
<Calendar
mode="single"
selected={day}
onSelect={setDay}
defaultMonth={new Date(2026, 8)}
captionLayout="dropdown"
startMonth={new Date(2024, 0)}
endMonth={new Date(2028, 11)}
disabled={{ dayOfWeek: [0, 6] }}
showWeekNumber
/>
</div>
<div className="w-fit max-w-full rounded-xl border bg-card p-3">
<Calendar
mode="multiple"
selected={days}
onSelect={setDays}
defaultMonth={new Date(2026, 8)}
max={4}
footer={`${days?.length ?? 0} of 4 days selected`}
/>
</div>
</div>
);
}API reference
| Prop | Type | Default |
|---|---|---|
borderedDraw a border, padding and background around the calendar. Turn off inside a popover or card. | boolean | false |
Also accepts the standard attributes of its root element.
Accessibility
| Key | Action |
|---|---|
| Arrow keys | Move between days |
| PageUp / PageDown | Previous or next month; with Shift, previous or next year |
| Home / End | First or last day of the week |
| Enter / Space | Selects the focused day |
Use with AI
Use mode single, multiple or range with selected and onSelect. All DayPicker props work: disabled, numberOfMonths, captionLayout dropdown, locale. With the shadcn MCP server set up (guide), ask your agent:
Add the Ballmac UI Calendar (@ballmac/calendar) to this project with the shadcn MCP, then use it where it fits.
Use it for
- Choosing a date or range inside a card or popover
- Booking and scheduling
- Showing availability with disabled days
Not for
- A date field in a form; use date-picker
- Typing a range with inputs; use date-range-picker
Registry JSON: https://ui.ballmac.com/r/calendar.json
Credits
Based on shadcn/ui Calendar, adapted by Ballmac. Free to use in personal and commercial projects.
- Registry
- @ballmac/directionshadcn/utils
Pairs well with
Popover
A focus-managed compact surface for controls or details, with collision handling, mobile-safe width, and optional close action.
Date Picker
A button that opens a calendar popover to choose one date, with presets, month and year selects, min and max dates, clearing, invalid state and form submission.
Button
A button with six variants, three sizes, a pill shape and a built-in loading state. buttonVariants() styles links the same way.
Color Picker
A native color chooser paired with an editable hex field and a live swatch.