- Accordion
- Agent Thinking
- Alert
- Alert Dialog
- Announcement
- Aspect Ratio
- Attachment
- Avatar
- Badge
- Breadcrumb
- Bubble
- Button
- Button Group
- Calendar
- Card
- Carousel
- Checkbox
- Checkbox Card
- Code Block
- Collapsible
- Color Picker
- Combobox
- Command
- Composer
- Composer Loader
- Context Menu
- Data Grid
- Data Table
- Date Picker
- Dialog
- Direction
- Drawer
- Dropdown Menu
- Empty
- Field
- File Upload
- Hover Card
- Input
- Input Group
- Input Otp
- Item
- Kbd
- Label
- Marker
- Menubar
- Message
- Message Scroller
- Meter
- Native Select
- Navigation Menu
- Number Field
- Pagination
- Period Switcher
- Popover
- Progress
- Questionnaire
- Radio Card
- Radio Group
- Resizable
- Reveal
- Scroll Area
- Search Field
- Select
- Separator
- Sheet
- Sidebar
- Skeleton
- Slider
- Social Button
- Sonner
- Spinner
- Status Dot
- Switch
- Switch Card
- Table
- Tabs
- Tag Group
- Textarea
- Theme Toggle
- Time Field
- Toggle
- Toggle Group
- Tooltip
- Trace
- Tree View
- Agent Creator 01
- Agent Limits Card 01
- Agent Progress 01
- AI Chat 01
- AI Image Generation 01
- AI Profile 01
- Alerts Card 01
- Auth Card 01
- Calendar 01
- Composer 01
- Composer Attachments 01
- Composer Panel 01
- Finance Dashboard 01
- Home Dashboard 01
- Marketing Dashboard 01
- Medical Profile 01
- Meeting Scheduler 01
- Multi Agent Chat 01
- Notification Center 01
- Patient Info Card 01
- Project Board 01
- Questionnaire 01
- Settings Modal 01
- Sidebar 01
- Stat Cards 01
- Task List 01
- Web Search 01
- Activity Rings Card 01
- Area Chart Card 01
- Bar List Card 01
- Combo Chart Card 01
- Contributions Card 01
- Earnings Chart Card 01
- Funnel Chart Card 01
- Heatmap Chart Card 01
- Line Chart Card 01
- Most Active Days Card 01
- Orders Chart Card 01
- Radar Chart Card 01
- Radial Chart Card 01
- Revenue Chart Card 01
- Sankey Chart Card 01
- Scatter Chart Card 01
- Sleep Score Card 01
- Stage Bars Card 01
- Steps Card 01
November 2026
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
Delivery on Saturday, November 14
Installation
$ pnpm dlx shadcn@latest add @cobalt/calendar
Ranges
mode="range" picks a start and an end. Set numberOfMonths={2} for two months side by side.
November 2026
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
December 2026
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
Nights: 4
Limits and unavailable days
Days outside disabled fade and can't be picked. Days passed as unavailable stay in the grid, struck through, so the rhythm of the month reads at a glance.
November 2026
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
Weekends are struck through
Right to left
Pass dir="rtl" and a locale. The chevrons mirror and the weekday names follow the locale.
نوفمبر 2026
| س | ح | ن | ث | ر | خ | ج |
|---|---|---|---|---|---|---|
Source
"use client";
import { RiArrowDownSLine } from "@remixicon/react";
import { cn } from "cn";
import * as React from "react";
import { DayPicker, getDefaultClassNames } from "react-day-picker";
import type {
ChevronProps,
DayButton,
Locale,
DateRange,
Matcher,
RootProps,
WeekNumberProps,
} from "react-day-picker";
import { Button, buttonVariants } from "@/components/ui/button";
// A month on the page rather than in a popover: the 326px card, the 16px month
// chevrons, the 32px day cells 12px apart with the selected day's accent pill
// and the range band that bridges those gaps. Today can carry a small accent
// dot, and days passed as `unavailable` are struck through. Colours come from
// theme-cobalt with semantic fallbacks, so the calendar also works without it.
const navButtonClass =
"size-4 cursor-pointer rounded-[3px] p-0 text-[var(--foreground-secondary,var(--muted-foreground))] select-none hover:bg-[var(--calendar-hover,var(--calendar-day-hover,var(--surface-strong)))] dark:hover:bg-[var(--calendar-hover,var(--calendar-day-hover,var(--surface-strong)))] motion-reduce:transition-none [&_svg]:size-4";
// Day cells read these from context rather than closing over Calendar's props:
// a component created inside Calendar gets a new identity on every render, so
// React remounted every day on each click and the hover and selection fades,
// and the keyboard focus, were thrown away with it.
const CalendarContext = React.createContext<{
locale?: Partial<Locale>;
showToday: boolean;
}>({ showToday: false });
function CalendarChevron({ className, orientation, ...props }: ChevronProps) {
// The month chevrons are a 16px glyph drawn with a 2px round-capped stroke,
// heavier than the Remix arrows the dropdown uses.
if (orientation === "left" || orientation === "right") {
return (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
aria-hidden
className={cn("size-4", className)}
{...props}
>
<path
d={
orientation === "left"
? "M9 4L5.70711 7.29289C5.31658 7.68342 5.31658 8.31658 5.70711 8.70711L9 12"
: "M7 4L10.2929 7.29289C10.6834 7.68342 10.6834 8.31658 10.2929 8.70711L7 12"
}
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
/>
</svg>
);
}
return <RiArrowDownSLine className={cn("size-4", className)} {...props} />;
}
function CalendarRoot({ className, rootRef, ...props }: RootProps) {
return (
<div
data-slot="calendar"
ref={rootRef}
className={cn(className)}
{...props}
/>
);
}
function CalendarWeekNumber({ children, ...props }: WeekNumberProps) {
return (
<td {...props}>
<div className="flex size-(--cell-size) items-center justify-center text-center">
{children}
</div>
</td>
);
}
function CalendarDay(props: React.ComponentProps<typeof DayButton>) {
const { locale, showToday } = React.useContext(CalendarContext);
return <CalendarDayButton locale={locale} showToday={showToday} {...props} />;
}
// A range is picked in two clicks, and the band follows the pointer in between
// (React Aria's RangeCalendar). react-day-picker would instead stretch the
// existing range towards the first click, so range mode keeps its own anchor
// and hands the finished range to `onSelect` on the second click.
function orderRange(a: Date, b: Date): DateRange {
return a <= b ? { from: a, to: b } : { from: b, to: a };
}
function useRangeAnchor(
props: React.ComponentProps<typeof DayPicker>
): Partial<React.ComponentProps<typeof DayPicker>> {
const range = props as React.ComponentProps<typeof DayPicker> & {
mode: "range";
};
const isRange = props.mode === "range";
const [anchor, setAnchor] = React.useState<Date | undefined>();
const [hovered, setHovered] = React.useState<Date | undefined>();
const [uncontrolled, setUncontrolled] = React.useState<DateRange | undefined>(
() =>
isRange
? (props as { defaultSelected?: DateRange }).defaultSelected
: undefined
);
React.useEffect(() => {
if (!anchor) {
return;
}
const cancel = (event: KeyboardEvent) => {
if (event.key === "Escape") {
setAnchor(undefined);
setHovered(undefined);
}
};
document.addEventListener("keydown", cancel);
return () => document.removeEventListener("keydown", cancel);
}, [anchor]);
if (!isRange) {
return {};
}
const committed = "selected" in range ? range.selected : uncontrolled;
const preview = anchor ? orderRange(anchor, hovered ?? anchor) : committed;
return {
selected: preview,
// A no-op keeps react-day-picker reading `selected` from props.
onSelect: () => {},
onDayClick: (day, modifiers, event) => {
range.onDayClick?.(day, modifiers, event);
if (!anchor) {
setAnchor(day);
setHovered(day);
return;
}
const next = orderRange(anchor, day);
setAnchor(undefined);
setHovered(undefined);
setUncontrolled(next);
(range.onSelect as ((...args: unknown[]) => void) | undefined)?.(
next,
day,
modifiers,
event
);
},
onDayMouseEnter: (day, modifiers, event) => {
range.onDayMouseEnter?.(day, modifiers, event);
if (anchor) {
setHovered(day);
}
},
onDayFocus: (day, modifiers, event) => {
range.onDayFocus?.(day, modifiers, event);
if (anchor) {
setHovered(day);
}
},
} as Partial<React.ComponentProps<typeof DayPicker>>;
}
function Calendar({
className,
classNames,
showOutsideDays = true,
captionLayout = "label",
buttonVariant = "ghost",
locale,
formatters,
components,
showToday = false,
unavailable,
disabled,
modifiers,
...props
}: React.ComponentProps<typeof DayPicker> & {
buttonVariant?: React.ComponentProps<typeof Button>["variant"];
/** Mark today with a dot under the date. */
showToday?: boolean;
/** Days that can be focused but not picked, drawn struck through. */
unavailable?: Matcher | Matcher[];
}) {
const defaultClassNames = getDefaultClassNames();
// Two-letter weekday names only work in Latin scripts: cut down, every
// Arabic name keeps just its article. Other scripts take the locale's
// narrow names instead.
const formatWeekday = React.useMemo(() => {
const short = new Intl.DateTimeFormat(locale?.code, { weekday: "short" });
const latin = /^\p{Script=Latin}/u.test(short.format(new Date(2024, 0, 7)));
const formatter = latin
? short
: new Intl.DateTimeFormat(locale?.code, { weekday: "narrow" });
return (day: Date) => {
const label = formatter.format(day);
return latin ? label.slice(0, 2) : label;
};
}, [locale?.code]);
// `unavailable` mirrors React Aria's isDateUnavailable: the day is struck
// through and cannot be picked, so it also joins the disabled matchers.
const disabledMatchers = React.useMemo(() => {
if (disabled === undefined) {
return unavailable;
}
if (unavailable === undefined) {
return disabled;
}
return [disabled, unavailable].flat();
}, [disabled, unavailable]);
const context = React.useMemo(
() => ({ locale, showToday }),
[locale, showToday]
);
const rangeProps = useRangeAnchor(props);
return (
<CalendarContext.Provider value={context}>
<DayPicker
showOutsideDays={showOutsideDays}
className={cn(
"group/calendar [--cell-radius:8px] [--cell-size:32px]",
className
)}
captionLayout={captionLayout}
locale={locale}
navLayout="around"
disabled={disabledMatchers}
modifiers={unavailable ? { ...modifiers, unavailable } : modifiers}
formatters={{
formatWeekdayName: formatWeekday,
formatMonthDropdown: (date) =>
date.toLocaleString(locale?.code, { month: "short" }),
...formatters,
}}
classNames={{
button_next: cn(
buttonVariants({ variant: buttonVariant }),
navButtonClass,
"absolute end-[15px] top-[15px] z-10",
defaultClassNames.button_next
),
button_previous: cn(
buttonVariants({ variant: buttonVariant }),
navButtonClass,
"absolute start-[15px] top-[15px] z-10",
defaultClassNames.button_previous
),
caption_label: cn(
"font-medium select-none",
captionLayout === "label"
? "text-sm"
: "flex items-center gap-1 rounded-(--cell-radius) text-sm [&>svg]:size-3.5 [&>svg]:text-muted-foreground",
defaultClassNames.caption_label
),
day: cn(
"group/day relative size-8 p-0 text-center select-none before:pointer-events-none before:absolute before:inset-y-0 before:-inset-x-1.5 before:bg-[var(--calendar-range,var(--accent))] before:opacity-0 before:transition-[opacity,border-radius] before:duration-100 before:ease-out motion-reduce:before:transition-none first:before:start-0 first:before:rounded-s-[8px] last:before:end-0 last:before:rounded-e-[8px]",
defaultClassNames.day
),
disabled: cn("text-muted-foreground", defaultClassNames.disabled),
dropdown: cn(
"absolute inset-0 bg-popover opacity-0",
defaultClassNames.dropdown
),
dropdown_root: cn(
"relative rounded-(--cell-radius)",
defaultClassNames.dropdown_root
),
dropdowns: cn(
"flex h-(--cell-size) w-full items-center justify-center gap-1.5 text-sm font-medium",
defaultClassNames.dropdowns
),
hidden: cn("invisible", defaultClassNames.hidden),
month: cn(
"relative flex w-[326px] shrink-0 flex-col gap-5 rounded-[16px] bg-popover p-[15px] shadow-[shadow:var(--shadow-control,0_1px_2px_0_rgb(0_0_0/0.05))]",
defaultClassNames.month
),
month_caption: cn(
"flex h-4 w-full items-center justify-center px-8",
defaultClassNames.month_caption
),
month_grid: cn(
"-m-3 border-separate [border-spacing:12px_12px]",
defaultClassNames.month_grid
),
months: cn(
"relative flex flex-wrap justify-center gap-2",
defaultClassNames.months
),
nav: cn(
"absolute inset-x-[15px] top-[15px] z-10 flex items-center justify-between gap-1",
defaultClassNames.nav
),
outside: cn("text-muted-foreground", defaultClassNames.outside),
range_end: cn(
"before:end-1/2 before:opacity-100",
defaultClassNames.range_end
),
range_middle: cn(
"before:opacity-100",
defaultClassNames.range_middle
),
range_start: cn(
"before:start-1/2 before:opacity-100 [&.rdp-range_end]:before:opacity-0",
defaultClassNames.range_start
),
root: cn("w-fit", defaultClassNames.root),
today: cn("", defaultClassNames.today),
week: cn("", defaultClassNames.week),
week_number: cn(
"text-[0.8rem] text-muted-foreground select-none",
defaultClassNames.week_number
),
week_number_header: cn(
"w-(--cell-size) select-none",
defaultClassNames.week_number_header
),
weekday: cn(
"size-6 p-0 text-center text-sm font-medium text-[var(--foreground-secondary,var(--muted-foreground))] select-none",
defaultClassNames.weekday
),
weekdays: cn("", defaultClassNames.weekdays),
...classNames,
}}
components={{
Chevron: CalendarChevron,
DayButton: CalendarDay,
Root: CalendarRoot,
WeekNumber: CalendarWeekNumber,
...components,
}}
{...({ ...props, ...rangeProps } as React.ComponentProps<
typeof DayPicker
>)}
/>
</CalendarContext.Provider>
);
}
function CalendarDayButton({
className,
day,
modifiers,
locale,
showToday = false,
children,
...props
}: React.ComponentProps<typeof DayButton> & {
locale?: Partial<Locale>;
showToday?: boolean;
}) {
const defaultClassNames = getDefaultClassNames();
const ref = React.useRef<HTMLButtonElement>(null);
React.useEffect(() => {
if (modifiers.focused) {
ref.current?.focus();
}
}, [modifiers.focused]);
const isSelectedSingle =
modifiers.selected &&
!modifiers.range_start &&
!modifiers.range_end &&
!modifiers.range_middle;
// The dot under today inverts on the accent pill so it stays visible there.
const onEdge =
isSelectedSingle || modifiers.range_start || modifiers.range_end;
return (
<Button
ref={ref}
variant="ghost"
size="icon"
data-day={day.date.toLocaleDateString(locale?.code)}
data-selected-single={isSelectedSingle}
data-range-start={modifiers.range_start}
data-range-end={modifiers.range_end}
data-range-middle={modifiers.range_middle}
data-unavailable={modifiers.unavailable}
className={cn(
// Hover is a surface behind the day, so a selected day keeps its pill:
// each selected state re-points --calendar-hover at the colour the
// cell already has instead of fighting the hover class. The selected
// fill sits on a `before:` layer so it switches instantly instead of
// fading with the hover transition.
"relative isolate z-10 size-8 before:absolute before:inset-0 before:-z-10 before:rounded-[inherit] min-w-8 rounded-[8px] border-0 p-0 text-sm leading-5 font-medium text-foreground transition-[background-color,border-radius] duration-100 ease-out hover:bg-[var(--calendar-hover,var(--calendar-day-hover,var(--surface-strong)))] focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring focus-visible:ring-offset-0 disabled:opacity-100 disabled:text-[var(--foreground-faint,var(--muted-foreground))] data-[unavailable=true]:line-through data-[range-end=true]:rounded-s-none data-[range-end=true]:before:bg-[var(--calendar-range-edge,var(--accent))] data-[range-middle=true]:bg-transparent data-[range-start=true]:rounded-e-none data-[range-start=true]:before:bg-[var(--calendar-range-edge,var(--accent))] data-[selected-single=true]:before:bg-[var(--calendar-range-edge,var(--accent))] data-[range-start=true]:data-[range-end=true]:rounded-[8px] data-[range-middle=true]:[--calendar-hover:transparent] data-[range-end=true]:[--calendar-hover:var(--calendar-range-edge,var(--accent))] data-[range-start=true]:[--calendar-hover:var(--calendar-range-edge,var(--accent))] data-[selected-single=true]:[--calendar-hover:var(--calendar-range-edge,var(--accent))] dark:hover:bg-[var(--calendar-hover,var(--calendar-day-hover,var(--surface-strong)))] motion-reduce:transition-none motion-reduce:active:scale-100",
defaultClassNames.day,
className
)}
{...props}
>
{children}
{showToday && modifiers.today ? (
<span
aria-hidden
data-slot="calendar-today-dot"
className={cn(
"pointer-events-none absolute bottom-1 left-1/2 size-1 -translate-x-1/2 rounded-full",
onEdge ? "bg-foreground" : "bg-ring"
)}
/>
) : null}
</Button>
);
}
export { Calendar, CalendarDayButton };