Skip to content
edmi-ui

Calendar

Month grid for picking a date or a range. Today is tinted; the selected day is a primary fill (elevation raises it).

StackBlitz ↗StackBlitz ↗StackBlitz ↗
October 2026
October 2026
27
28
29
30
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
Event Date, October 2026
October 2026
SuMoTuWeThFrSa
27
28
29
30
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
Terminal window
npx shadcn@latest add @edmi-ui/calendar
Terminal window
pnpm dlx shadcn@latest add @edmi-ui/calendar
Terminal window
yarn dlx shadcn@latest add @edmi-ui/calendar
Terminal window
bunx shadcn@latest add @edmi-ui/calendar

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

Terminal window
npx shadcn-vue@latest add @edmi-ui/calendar
Terminal window
pnpm dlx shadcn-vue@latest add @edmi-ui/calendar
Terminal window
yarn dlx shadcn-vue@latest add @edmi-ui/calendar
Terminal window
bunx shadcn-vue@latest add @edmi-ui/calendar

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

Terminal window
npx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/calendar.json
Terminal window
pnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/calendar.json
Terminal window
yarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/calendar.json
Terminal window
bunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/calendar.json

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

import { Calendar } from "@/components/ui/calendar";
<Calendar
mode="single"
className="rounded-xl border border-border"
/>
<script setup lang="ts">
import { Calendar } from "@/components/ui/calendar";
</script>
<template>
<Calendar class="rounded-xl border border-border" />
</template>
<script lang="ts">
import { Calendar } from "$lib/components/ui/calendar";
</script>
<Calendar type="single" bind:value class="rounded-xl border border-border" />
registry/ui/calendar.tsx
"use client";
import { cn } from "cn";
import * as React from "react";
import {
type DayButton,
DayPicker,
getDefaultClassNames,
type Locale,
} from "react-day-picker";
import { CaretLeftIcon, CaretRightIcon, CaretDownIcon } from "@phosphor-icons/react";
import { Button, buttonVariants } from "@/registry/edmi/ui/button";
import { type Elevation, useElevation } from "@/registry/edmi/ui/elevation";
// Selected day (single, range start/end) is a flat --primary fill. ✦ `elevation` sits on the calendar SHELL
// (sunken / raised / floating); when raised or floating the selected day rises with the handle recipe.
// Class strings are literal for Tailwind's scanner.
const shellElevation = {
sunken: "rounded-xl border border-sk-bd bg-sk-bg shadow-sunken",
flat: "",
raised: "rounded-xl border border-transparent shadow-raised",
floating: "rounded-xl border border-transparent shadow-floating",
};
function Calendar({
className,
classNames,
showOutsideDays = true,
captionLayout = "label",
buttonVariant = "outline",
locale,
formatters,
components,
elevation,
...props
}: React.ComponentProps<typeof DayPicker> & {
buttonVariant?: React.ComponentProps<typeof Button>["variant"];
/** ✦ depth of the calendar shell: sunken -1, flat 0, raised +1, floating +2 (selected day rises when raised). */
elevation?: Elevation;
}) {
const defaultClassNames = getDefaultClassNames();
const shell = useElevation(elevation, "surface");
const handle = useElevation(
elevation === "sunken" ? "flat" : elevation,
"handle",
);
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn(
"group/calendar bg-card p-3 [--cell-radius:8px] [--cell-size:36px] in-data-[slot=card-content]:bg-transparent in-data-[slot=popover-content]:bg-transparent",
String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
shellElevation[shell],
"in-data-[slot=popover-content]:border-0 in-data-[slot=popover-content]:shadow-none",
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-3", 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-7 rounded-md p-0 select-none aria-disabled:opacity-50",
defaultClassNames.button_previous,
),
button_next: cn(
buttonVariants({ variant: buttonVariant }),
"size-7 rounded-md p-0 select-none aria-disabled:opacity-50",
defaultClassNames.button_next,
),
month_caption: cn(
"flex h-7 w-full items-center justify-center px-9",
defaultClassNames.month_caption,
),
dropdowns: cn(
"flex h-7 w-full items-center justify-center gap-1.5 text-[13px] font-medium",
defaultClassNames.dropdowns,
),
dropdown_root: cn(
"relative rounded-md border border-input bg-card has-focus:border-ring has-focus:shadow-ring",
defaultClassNames.dropdown_root,
),
dropdown: cn(
"absolute inset-0 bg-popover opacity-0",
defaultClassNames.dropdown,
),
caption_label: cn(
"font-semibold select-none",
captionLayout === "label"
? "text-sm"
: "flex h-7 items-center gap-1 rounded-md pr-1.5 pl-2 text-[13px] font-medium [&>svg]:size-3.5 [&>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 h-7 flex-1 items-center justify-center text-[11.5px] font-normal text-muted-foreground select-none",
defaultClassNames.weekday,
),
week: cn("flex w-full", defaultClassNames.week),
week_number_header: cn(
"w-(--cell-size) select-none",
defaultClassNames.week_number_header,
),
week_number: cn(
"text-[11.5px] text-muted-foreground select-none",
defaultClassNames.week_number,
),
day: cn(
"group/day relative aspect-square h-full w-full p-0 text-center select-none",
defaultClassNames.day,
),
range_start: cn(
"relative isolate z-0 rounded-l-(--cell-radius) bg-accent",
defaultClassNames.range_start,
),
range_middle: cn("rounded-none", defaultClassNames.range_middle),
range_end: cn(
"relative isolate z-0 rounded-r-(--cell-radius) bg-accent",
defaultClassNames.range_end,
),
today: cn("rounded-(--cell-radius)", defaultClassNames.today),
outside: cn(
"text-muted-foreground opacity-50 aria-selected:text-muted-foreground",
defaultClassNames.outside,
),
disabled: cn(
"text-muted-foreground opacity-50",
defaultClassNames.disabled,
),
hidden: cn("invisible", defaultClassNames.hidden),
...classNames,
}}
components={{
Root: ({ className, rootRef, ...props }) => {
return (
<div
data-slot="calendar"
ref={rootRef}
className={cn(className)}
{...props}
/>
);
},
Chevron: ({ className, orientation, ...props }) => {
if (orientation === "left") {
return (
<CaretLeftIcon
className={cn("size-4", className)}
{...props}
/>
);
}
if (orientation === "right") {
return (
<CaretRightIcon
className={cn("size-4", className)}
{...props}
/>
);
}
return (
<CaretDownIcon
className={cn("size-4", className)}
{...props}
/>
);
},
DayButton: ({ ...props }) => (
<CalendarDayButton locale={locale} elevation={handle} {...props} />
),
WeekNumber: ({ children, ...props }) => {
return (
<td {...props}>
<div className="flex size-(--cell-size) items-center justify-center text-center">
{children}
</div>
</td>
);
},
...components,
}}
{...props}
/>
);
}
function CalendarDayButton({
className,
day,
modifiers,
locale,
elevation = "flat",
...props
}: React.ComponentProps<typeof DayButton> & {
locale?: Partial<Locale>;
/** selected day depth: raised or floating make it rise */
elevation?: Elevation;
}) {
const raised = elevation === "raised" || elevation === "floating";
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}
data-today={modifiers.today && !modifiers.selected}
className={cn(
"relative isolate z-10 flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 rounded-(--cell-radius) border-0 text-[13px] leading-none font-normal data-[today=true]:bg-accent data-[today=true]:font-semibold data-[today=true]:text-foreground group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:shadow-ring data-[range-middle=true]:rounded-none data-[range-middle=true]:bg-accent data-[range-middle=true]:text-foreground [&>span]:text-xs [&>span]:opacity-70",
"data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[selected-single=true]:hover:bg-primary data-[selected-single=true]:hover:text-primary-foreground",
"data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-start=true]:hover:bg-primary data-[range-start=true]:hover:text-primary-foreground",
"data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground data-[range-end=true]:hover:bg-primary data-[range-end=true]:hover:text-primary-foreground",
raised &&
"data-[selected-single=true]:border-transparent data-[selected-single=true]:[background-image:var(--r1-p-face)] data-[selected-single=true]:shadow-btn-raised-primary data-[range-start=true]:border-transparent data-[range-start=true]:[background-image:var(--r1-p-face)] data-[range-start=true]:shadow-btn-raised-primary data-[range-end=true]:border-transparent data-[range-end=true]:[background-image:var(--r1-p-face)] data-[range-end=true]:shadow-btn-raised-primary",
defaultClassNames.day,
className,
)}
{...props}
/>
);
}
export { Calendar, CalendarDayButton };
registry/ui/calendar/Calendar.vue
<script lang="ts" setup>
import type { CalendarRootEmits, CalendarRootProps, DateValue } from "reka-ui"
import type { HTMLAttributes, Ref } from "vue"
import type { LayoutTypes } from "."
import { getLocalTimeZone, today } from "@internationalized/date"
import { ChevronLeft, ChevronRight } from "@lucide/vue"
import { reactiveOmit, useVModel } from "@vueuse/core"
import {
CalendarCell,
CalendarCellTrigger,
CalendarGrid,
CalendarGridBody,
CalendarGridHead,
CalendarGridRow,
CalendarHeadCell,
CalendarHeader,
CalendarHeading,
CalendarNext,
CalendarPrev,
CalendarRoot,
useForwardPropsEmits,
} from "reka-ui"
import { computed } from "vue"
import { cn } from "@/registry/edmi/lib/utils"
import { buttonVariants } from "@/registry/edmi/ui/button"
import { type Elevation, useElevation } from "@/registry/edmi/ui/elevation"
import CalendarCaption from "./CalendarCaption.vue"
import {
calendarCellClass,
calendarHeadCellClass,
calendarNavButtonClass,
calendarRootClass,
calendarShellElevation,
calendarShellInHost,
calendarSingleSelectedClass,
calendarSingleSelectedRaisedClass,
calendarTriggerClass,
} from "./classes"
const props = withDefaults(
defineProps<
CalendarRootProps & {
class?: HTMLAttributes["class"]
/** ✦ month/year dropdowns instead of a text heading. */
layout?: LayoutTypes
yearRange?: DateValue[]
/** ✦ depth of the calendar shell: sunken -1, flat 0, raised +1, floating +2 (selected day rises when raised) */
elevation?: Elevation
}
>(),
{ modelValue: undefined, layout: undefined, elevation: undefined },
)
const emits = defineEmits<CalendarRootEmits>()
const delegatedProps = reactiveOmit(props, "class", "layout", "placeholder", "yearRange", "elevation")
const shell = useElevation(() => props.elevation, "surface")
const handle = useElevation(() => (props.elevation === "sunken" ? "flat" : props.elevation), "handle")
const raised = computed(() => handle.value === "raised" || handle.value === "floating")
const placeholder = useVModel(props, "placeholder", emits, {
passive: true,
defaultValue: props.defaultPlaceholder ?? today(getLocalTimeZone()),
}) as Ref<DateValue>
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<CalendarRoot
v-slot="{ grid, weekDays, date }"
v-bind="forwarded"
:weekday-format="props.weekdayFormat ?? 'short'"
v-model:placeholder="placeholder"
data-slot="calendar"
:class="cn(calendarRootClass, calendarShellElevation[shell], calendarShellInHost, props.class)"
>
<CalendarHeader data-slot="calendar-header" class="relative flex h-7 w-full items-center justify-center px-9">
<CalendarCaption
v-if="layout"
:date="date"
:layout="layout"
:locale="props.locale"
:year-range="props.yearRange"
:min-value="props.minValue"
:max-value="props.maxValue"
@update:placeholder="(v) => (placeholder = v)"
/>
<CalendarHeading v-else data-slot="calendar-heading" class="text-sm font-semibold select-none" />
<nav class="pointer-events-none absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1">
<CalendarPrev
data-slot="calendar-prev-button"
:class="cn(buttonVariants({ variant: 'outline' }), calendarNavButtonClass)"
>
<slot name="calendar-prev-icon"><ChevronLeft class="size-4" /></slot>
</CalendarPrev>
<CalendarNext
data-slot="calendar-next-button"
:class="cn(buttonVariants({ variant: 'outline' }), calendarNavButtonClass)"
>
<slot name="calendar-next-icon"><ChevronRight class="size-4" /></slot>
</CalendarNext>
</nav>
</CalendarHeader>
<div class="mt-3 flex flex-col gap-4 sm:flex-row">
<CalendarGrid v-for="month in grid" :key="month.value.toString()" data-slot="calendar-grid" class="w-full border-collapse">
<CalendarGridHead>
<CalendarGridRow class="flex">
<CalendarHeadCell v-for="day in weekDays" :key="day" :class="calendarHeadCellClass">
{{ day.slice(0, 2) }}
</CalendarHeadCell>
</CalendarGridRow>
</CalendarGridHead>
<CalendarGridBody>
<CalendarGridRow v-for="(weekDates, index) in month.rows" :key="`weekDate-${index}`" class="flex w-full">
<CalendarCell
v-for="weekDate in weekDates"
:key="weekDate.toString()"
:date="weekDate"
data-slot="calendar-cell"
:class="calendarCellClass"
>
<CalendarCellTrigger
:day="weekDate"
:month="month.value"
data-slot="calendar-cell-trigger"
:class="cn(buttonVariants({ variant: 'ghost', size: 'icon' }), calendarTriggerClass, calendarSingleSelectedClass, raised && calendarSingleSelectedRaisedClass)"
/>
</CalendarCell>
</CalendarGridRow>
</CalendarGridBody>
</CalendarGrid>
</div>
</CalendarRoot>
</template>
registry/ui/calendar/CalendarCaption.vue
<script setup lang="ts">
import type { DateValue } from "reka-ui"
import type { LayoutTypes } from "."
import { ChevronDown } from "@lucide/vue"
import { useDateFormatter } from "reka-ui"
import { createYear, createYearRange, toDate } from "reka-ui/date"
import { computed } from "vue"
// ✦ Month/year dropdown caption (React `captionLayout="dropdown"`): a styled native <select> under a label.
const props = defineProps<{
date: DateValue
layout?: LayoutTypes
locale?: string
yearRange?: DateValue[]
minValue?: DateValue
maxValue?: DateValue
}>()
const emit = defineEmits<{ (e: "update:placeholder", value: DateValue): void }>()
const formatter = useDateFormatter(props.locale ?? "en")
const years = computed(
() =>
props.yearRange ??
createYearRange({
start: props.minValue ?? props.date.cycle("year", -100),
end: props.maxValue ?? props.date.cycle("year", 10),
}),
)
const months = computed(() => createYear({ dateObj: props.date }))
const wrap =
"relative rounded-md border border-input bg-card has-focus:border-ring has-focus:shadow-ring"
const label =
"pointer-events-none flex h-7 items-center gap-1 rounded-md pr-1.5 pl-2 text-[13px] font-medium [&>svg]:size-3.5 [&>svg]:text-muted-foreground"
const select = "absolute inset-0 w-full cursor-pointer bg-popover opacity-0"
</script>
<template>
<div class="flex h-7 w-full items-center justify-center gap-1.5">
<div v-if="layout !== 'year-only'" :class="wrap">
<div :class="label">
{{ formatter.custom(toDate(date), { month: "short" }) }}
<ChevronDown />
</div>
<select
:class="select"
aria-label="Month"
:value="date.month"
@change="(e: Event) => emit('update:placeholder', date.set({ month: Number((e.target as HTMLSelectElement).value) }))"
>
<option v-for="m in months" :key="m.toString()" :value="m.month">
{{ formatter.custom(toDate(m), { month: "short" }) }}
</option>
</select>
</div>
<span v-else class="text-[13px] font-medium">
{{ formatter.custom(toDate(date), { month: "short" }) }}
</span>
<div v-if="layout !== 'month-only'" :class="wrap">
<div :class="label">
{{ formatter.custom(toDate(date), { year: "numeric" }) }}
<ChevronDown />
</div>
<select
:class="select"
aria-label="Year"
:value="date.year"
@change="(e: Event) => emit('update:placeholder', date.set({ year: Number((e.target as HTMLSelectElement).value) }))"
>
<option v-for="y in years" :key="y.toString()" :value="y.year">
{{ formatter.custom(toDate(y), { year: "numeric" }) }}
</option>
</select>
</div>
<span v-else class="text-[13px] font-medium">
{{ formatter.custom(toDate(date), { year: "numeric" }) }}
</span>
</div>
</template>
registry/ui/calendar/RangeCalendar.vue
<script lang="ts" setup>
import type { DateValue, RangeCalendarRootEmits, RangeCalendarRootProps } from "reka-ui"
import type { HTMLAttributes, Ref } from "vue"
import type { LayoutTypes } from "."
import { getLocalTimeZone, today } from "@internationalized/date"
import { ChevronLeft, ChevronRight } from "@lucide/vue"
import { reactiveOmit, useVModel } from "@vueuse/core"
import {
RangeCalendarCell,
RangeCalendarCellTrigger,
RangeCalendarGrid,
RangeCalendarGridBody,
RangeCalendarGridHead,
RangeCalendarGridRow,
RangeCalendarHeadCell,
RangeCalendarHeader,
RangeCalendarHeading,
RangeCalendarNext,
RangeCalendarPrev,
RangeCalendarRoot,
useForwardPropsEmits,
} from "reka-ui"
import { computed } from "vue"
import { cn } from "@/registry/edmi/lib/utils"
import { buttonVariants } from "@/registry/edmi/ui/button"
import { type Elevation, useElevation } from "@/registry/edmi/ui/elevation"
import CalendarCaption from "./CalendarCaption.vue"
import {
calendarHeadCellClass,
calendarNavButtonClass,
calendarRangeCellClass,
calendarRangeEdgeClass,
calendarRangeEdgeRaisedClass,
calendarRangeMiddleClass,
calendarRootClass,
calendarShellElevation,
calendarShellInHost,
calendarTriggerClass,
} from "./classes"
// ✦ Range selection (React `Calendar mode="range"`): start/end raised, days between on `bg-accent`.
const props = withDefaults(
defineProps<
RangeCalendarRootProps & {
class?: HTMLAttributes["class"]
layout?: LayoutTypes
yearRange?: DateValue[]
/** ✦ depth of the calendar shell: sunken -1, flat 0, raised +1, floating +2 (selected day rises when raised) */
elevation?: Elevation
}
>(),
{ modelValue: undefined, layout: undefined, elevation: undefined },
)
const emits = defineEmits<RangeCalendarRootEmits>()
const delegatedProps = reactiveOmit(props, "class", "layout", "placeholder", "yearRange", "elevation")
const shell = useElevation(() => props.elevation, "surface")
const handle = useElevation(() => (props.elevation === "sunken" ? "flat" : props.elevation), "handle")
const raised = computed(() => handle.value === "raised" || handle.value === "floating")
const placeholder = useVModel(props, "placeholder", emits, {
passive: true,
defaultValue: props.defaultPlaceholder ?? today(getLocalTimeZone()),
}) as Ref<DateValue>
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<RangeCalendarRoot
v-slot="{ grid, weekDays, date }"
v-bind="forwarded"
:weekday-format="props.weekdayFormat ?? 'short'"
v-model:placeholder="placeholder"
data-slot="calendar"
:class="cn(calendarRootClass, calendarShellElevation[shell], calendarShellInHost, props.class)"
>
<RangeCalendarHeader data-slot="calendar-header" class="relative flex h-7 w-full items-center justify-center px-9">
<CalendarCaption
v-if="layout"
:date="date"
:layout="layout"
:locale="props.locale"
:year-range="props.yearRange"
:min-value="props.minValue"
:max-value="props.maxValue"
@update:placeholder="(v) => (placeholder = v)"
/>
<RangeCalendarHeading v-else data-slot="calendar-heading" class="text-sm font-semibold select-none" />
<nav class="pointer-events-none absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1">
<RangeCalendarPrev
data-slot="calendar-prev-button"
:class="cn(buttonVariants({ variant: 'outline' }), calendarNavButtonClass)"
>
<slot name="calendar-prev-icon"><ChevronLeft class="size-4" /></slot>
</RangeCalendarPrev>
<RangeCalendarNext
data-slot="calendar-next-button"
:class="cn(buttonVariants({ variant: 'outline' }), calendarNavButtonClass)"
>
<slot name="calendar-next-icon"><ChevronRight class="size-4" /></slot>
</RangeCalendarNext>
</nav>
</RangeCalendarHeader>
<div class="mt-3 flex flex-col gap-4 sm:flex-row">
<RangeCalendarGrid v-for="month in grid" :key="month.value.toString()" data-slot="calendar-grid" class="w-full border-collapse">
<RangeCalendarGridHead>
<RangeCalendarGridRow class="flex">
<RangeCalendarHeadCell v-for="day in weekDays" :key="day" :class="calendarHeadCellClass">
{{ day.slice(0, 2) }}
</RangeCalendarHeadCell>
</RangeCalendarGridRow>
</RangeCalendarGridHead>
<RangeCalendarGridBody>
<RangeCalendarGridRow v-for="(weekDates, index) in month.rows" :key="`weekDate-${index}`" class="flex w-full">
<RangeCalendarCell
v-for="weekDate in weekDates"
:key="weekDate.toString()"
:date="weekDate"
data-slot="calendar-cell"
:class="calendarRangeCellClass"
>
<RangeCalendarCellTrigger
:day="weekDate"
:month="month.value"
data-slot="calendar-cell-trigger"
:class="cn(buttonVariants({ variant: 'ghost', size: 'icon' }), calendarTriggerClass, calendarRangeMiddleClass, calendarRangeEdgeClass, raised && calendarRangeEdgeRaisedClass)"
/>
</RangeCalendarCell>
</RangeCalendarGridRow>
</RangeCalendarGridBody>
</RangeCalendarGrid>
</div>
</RangeCalendarRoot>
</template>
registry/ui/calendar/classes.ts
// Class strings shared by Calendar and RangeCalendar. Literal for Tailwind's scanner.
// Selected day (single, range start/end): flat solid primary; the raised variants (v4 handle recipe) apply when the calendar elevation is raised or floating.
// The calendar SHELL takes the elevation (sunken / raised / floating), never the day grid.
export const calendarRootClass =
"group/calendar w-fit bg-card p-3 [--cell-radius:8px] [--cell-size:36px] in-data-[slot=card-content]:bg-transparent in-data-[slot=popover-content]:bg-transparent";
export const calendarNavButtonClass =
"pointer-events-auto size-7 rounded-md p-0 select-none disabled:opacity-50 rtl:rotate-180";
export const calendarHeadCellClass =
"flex h-7 flex-1 items-center justify-center text-[11.5px] font-normal text-muted-foreground select-none";
export const calendarCellClass =
"relative flex-1 p-0 text-center focus-within:relative focus-within:z-20";
export const calendarRangeCellClass =
"relative flex-1 p-0 text-center focus-within:relative focus-within:z-20 [&:has([data-selected])]:bg-accent [&:has([data-selection-start])]:rounded-l-(--cell-radius) [&:has([data-selection-end])]:rounded-r-(--cell-radius)";
export const calendarTriggerClass =
"relative isolate z-10 flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 rounded-(--cell-radius) border-0 text-[13px] leading-none font-normal cursor-default focus-visible:z-10 focus-visible:shadow-ring [&>span]:text-xs [&>span]:opacity-70 [&[data-today]:not([data-selected])]:bg-accent [&[data-today]:not([data-selected])]:font-semibold data-[disabled]:text-muted-foreground data-[disabled]:opacity-50 data-[outside-view]:text-muted-foreground data-[outside-view]:opacity-50 data-[unavailable]:text-muted-foreground data-[unavailable]:line-through";
export const calendarSingleSelectedClass =
"data-[selected]:bg-primary data-[selected]:text-primary-foreground data-[selected]:hover:bg-primary data-[selected]:hover:text-primary-foreground";
export const calendarShellElevation = {
sunken: "rounded-xl border border-sk-bd bg-sk-bg shadow-sunken",
flat: "",
raised: "rounded-xl border border-transparent shadow-raised",
floating: "rounded-xl border border-transparent shadow-floating",
};
// popover / card hosts own the shell: the calendar drops its own edge there
export const calendarShellInHost =
"in-data-[slot=popover-content]:border-0 in-data-[slot=popover-content]:shadow-none";
export const calendarSingleSelectedRaisedClass =
"data-[selected]:border-transparent data-[selected]:[background-image:var(--r1-p-face)] data-[selected]:shadow-btn-raised-primary";
export const calendarRangeEdgeClass =
"data-[selection-start]:bg-primary data-[selection-start]:text-primary-foreground data-[selection-start]:hover:bg-primary data-[selection-start]:hover:text-primary-foreground data-[selection-end]:bg-primary data-[selection-end]:text-primary-foreground data-[selection-end]:hover:bg-primary data-[selection-end]:hover:text-primary-foreground";
export const calendarRangeEdgeRaisedClass =
"data-[selection-start]:border-transparent data-[selection-start]:[background-image:var(--r1-p-face)] data-[selection-start]:shadow-btn-raised-primary data-[selection-end]:border-transparent data-[selection-end]:[background-image:var(--r1-p-face)] data-[selection-end]:shadow-btn-raised-primary";
export const calendarRangeMiddleClass =
"[&[data-selected]:not([data-selection-start]):not([data-selection-end])]:rounded-none [&[data-selected]:not([data-selection-start]):not([data-selection-end])]:bg-accent [&[data-selected]:not([data-selection-start]):not([data-selection-end])]:text-foreground data-[highlighted]:rounded-none data-[highlighted]:bg-accent";
registry/ui/calendar/index.ts
export { default as Calendar } from "./Calendar.vue";
export { default as CalendarCaption } from "./CalendarCaption.vue";
export { default as RangeCalendar } from "./RangeCalendar.vue";
export type LayoutTypes =
| "month-and-year"
| "month-only"
| "year-only"
| undefined;
src/lib/registry/ui/calendar/calendar.svelte
<script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui";
import CaretLeftIcon from "phosphor-svelte/lib/CaretLeft";
import CaretRightIcon from "phosphor-svelte/lib/CaretRight";
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
import { buttonVariants } from "$lib/registry/ui/button/index.js";
import { type Elevation, useElevation } from "$lib/registry/ui/elevation/index.js";
import CalendarCaption from "./calendar-caption.svelte";
import {
calendarCellClass,
calendarDayClass,
calendarHeadCellClass,
calendarNavButtonClass,
calendarRootClass,
calendarShellElevation,
calendarShellInHost,
calendarSingleSelectedClass,
calendarSingleSelectedRaisedClass,
} from "./classes.js";
let {
ref = $bindable(null),
value = $bindable(),
placeholder = $bindable(),
class: className,
weekdayFormat = "short",
captionLayout = "label",
locale = "en-US",
disableDaysOutsideMonth = false,
elevation = "auto",
...restProps
}: WithoutChildrenOrChild<CalendarPrimitive.RootProps> & {
/** ✦ `dropdown*` renders month/year selects instead of a text heading. */
captionLayout?: "dropdown" | "dropdown-months" | "dropdown-years" | "label";
/** ✦ depth of the calendar shell: sunken -1, flat 0, raised +1, floating +2 (selected day rises when raised). */
elevation?: Elevation;
} = $props();
const shell = useElevation(() => elevation, "surface");
const handle = useElevation(() => (elevation === "sunken" ? "flat" : elevation), "handle");
const raised = $derived(handle.current === "raised" || handle.current === "floating");
</script>
<!--
Discriminated Unions + Destructing (required for bindable) do not
get along, so we shut typescript up by casting `value` to `never`.
-->
<CalendarPrimitive.Root
bind:value={value as never}
bind:ref
bind:placeholder
{weekdayFormat}
{disableDaysOutsideMonth}
{locale}
data-slot="calendar"
class={cn(calendarRootClass, calendarShellElevation[shell.current], calendarShellInHost, className)}
{...restProps}
>
{#snippet children({ months, weekdays })}
<div class="relative flex flex-col gap-4 md:flex-row">
<nav
class="pointer-events-none absolute inset-x-0 top-0 flex h-7 w-full items-center justify-between gap-1"
>
<CalendarPrimitive.PrevButton
data-slot="calendar-prev-button"
class={cn(buttonVariants({ variant: "outline" }), calendarNavButtonClass)}
>
<CaretLeftIcon
class="size-4"
/>
</CalendarPrimitive.PrevButton>
<CalendarPrimitive.NextButton
data-slot="calendar-next-button"
class={cn(buttonVariants({ variant: "outline" }), calendarNavButtonClass)}
>
<CaretRightIcon
class="size-4"
/>
</CalendarPrimitive.NextButton>
</nav>
{#each months as month, monthIndex (month)}
<div class="flex w-full flex-col gap-3">
<CalendarPrimitive.Header
data-slot="calendar-header"
class="flex h-7 w-full items-center justify-center gap-1.5 px-9"
>
<CalendarCaption
{captionLayout}
month={month.value}
bind:placeholder
{locale}
{monthIndex}
minValue={restProps.minValue}
maxValue={restProps.maxValue}
/>
</CalendarPrimitive.Header>
<CalendarPrimitive.Grid data-slot="calendar-grid" class="w-full border-collapse">
<CalendarPrimitive.GridHead>
<CalendarPrimitive.GridRow class="flex select-none">
{#each weekdays as weekday, i (i)}
<CalendarPrimitive.HeadCell class={calendarHeadCellClass}>
{weekday.slice(0, 2)}
</CalendarPrimitive.HeadCell>
{/each}
</CalendarPrimitive.GridRow>
</CalendarPrimitive.GridHead>
<CalendarPrimitive.GridBody>
{#each month.weeks as weekDates (weekDates)}
<CalendarPrimitive.GridRow class="flex w-full">
{#each weekDates as date (date)}
<CalendarPrimitive.Cell
{date}
month={month.value}
data-slot="calendar-cell"
class={calendarCellClass}
>
<CalendarPrimitive.Day
data-slot="calendar-day"
class={cn(
buttonVariants({ variant: "ghost", size: "icon" }),
calendarDayClass,
calendarSingleSelectedClass,
raised && calendarSingleSelectedRaisedClass
)}
/>
</CalendarPrimitive.Cell>
{/each}
</CalendarPrimitive.GridRow>
{/each}
</CalendarPrimitive.GridBody>
</CalendarPrimitive.Grid>
</div>
{/each}
</div>
{/snippet}
</CalendarPrimitive.Root>
src/lib/registry/ui/calendar/calendar-caption.svelte
<script lang="ts">
import { DateFormatter, getLocalTimeZone, type DateValue } from "@internationalized/date";
import CaretDownIcon from "phosphor-svelte/lib/CaretDown";
// ✦ Month/year dropdown caption (React `captionLayout="dropdown"`): a styled native <select> under a label.
let {
captionLayout = "label",
month,
placeholder = $bindable(),
locale = "en-US",
monthIndex = 0,
minValue,
maxValue,
}: {
captionLayout?: "dropdown" | "dropdown-months" | "dropdown-years" | "label";
/** The month this caption is for (first of the displayed month). */
month: DateValue;
placeholder: DateValue | undefined;
locale?: string;
monthIndex?: number;
minValue?: DateValue;
maxValue?: DateValue;
} = $props();
const tz = getLocalTimeZone();
const monthFmt = $derived(new DateFormatter(locale, { month: "short" }));
const longFmt = $derived(new DateFormatter(locale, { month: "long" }));
const yearFmt = $derived(new DateFormatter(locale, { year: "numeric" }));
const months = $derived(
Array.from({ length: 12 }, (_, i) => {
const m = i + 1;
return { value: m, label: monthFmt.format(month.set({ month: m, day: 1 }).toDate(tz)) };
})
);
const years = $derived.by(() => {
const from = minValue?.year ?? month.year - 100;
const to = maxValue?.year ?? month.year + 10;
return Array.from({ length: to - from + 1 }, (_, i) => {
const y = from + i;
return { value: y, label: yearFmt.format(month.set({ year: y, day: 1 }).toDate(tz)) };
});
});
function setPlaceholder(part: { month: number } | { year: number }) {
if (!placeholder) return;
placeholder = placeholder.set(part).subtract({ months: monthIndex });
}
</script>
{#snippet Caret()}
<CaretDownIcon
/>
{/snippet}
{#snippet MonthSelect()}
<span
class="relative rounded-md border border-input bg-card has-focus:border-ring has-focus:shadow-ring"
>
<span
class="pointer-events-none flex h-7 items-center gap-1 rounded-md pr-1.5 pl-2 text-[13px] font-medium [&>svg]:size-3.5 [&>svg]:text-muted-foreground"
aria-hidden="true"
>
{monthFmt.format(month.toDate(tz))}
{@render Caret()}
</span>
<select
class="absolute inset-0 w-full cursor-pointer bg-popover opacity-0"
aria-label="Month"
value={month.month}
onchange={(e) => setPlaceholder({ month: Number.parseInt(e.currentTarget.value) })}
>
{#each months as m (m.value)}
<option value={m.value} selected={m.value === month.month}>{m.label}</option>
{/each}
</select>
</span>
{/snippet}
{#snippet YearSelect()}
<span
class="relative rounded-md border border-input bg-card has-focus:border-ring has-focus:shadow-ring"
>
<span
class="pointer-events-none flex h-7 items-center gap-1 rounded-md pr-1.5 pl-2 text-[13px] font-medium [&>svg]:size-3.5 [&>svg]:text-muted-foreground"
aria-hidden="true"
>
{yearFmt.format(month.toDate(tz))}
{@render Caret()}
</span>
<select
class="absolute inset-0 w-full cursor-pointer bg-popover opacity-0"
aria-label="Year"
value={month.year}
onchange={(e) => setPlaceholder({ year: Number.parseInt(e.currentTarget.value) })}
>
{#each years as y (y.value)}
<option value={y.value} selected={y.value === month.year}>{y.label}</option>
{/each}
</select>
</span>
{/snippet}
{#if captionLayout === "dropdown"}
{@render MonthSelect()}
{@render YearSelect()}
{:else if captionLayout === "dropdown-months"}
{@render MonthSelect()}
<span class="text-[13px] font-medium">{yearFmt.format(month.toDate(tz))}</span>
{:else if captionLayout === "dropdown-years"}
<span class="text-[13px] font-medium">{monthFmt.format(month.toDate(tz))}</span>
{@render YearSelect()}
{:else}
<span class="text-sm font-semibold select-none">
{longFmt.format(month.toDate(tz))}
{yearFmt.format(month.toDate(tz))}
</span>
{/if}
src/lib/registry/ui/calendar/range-calendar.svelte
<script lang="ts">
import { RangeCalendar as RangeCalendarPrimitive } from "bits-ui";
import CaretLeftIcon from "phosphor-svelte/lib/CaretLeft";
import CaretRightIcon from "phosphor-svelte/lib/CaretRight";
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
import { buttonVariants } from "$lib/registry/ui/button/index.js";
import { type Elevation, useElevation } from "$lib/registry/ui/elevation/index.js";
import CalendarCaption from "./calendar-caption.svelte";
import {
calendarDayClass,
calendarHeadCellClass,
calendarNavButtonClass,
calendarRangeCellClass,
calendarRangeEdgeClass,
calendarRangeEdgeRaisedClass,
calendarRangeMiddleClass,
calendarRootClass,
calendarShellElevation,
calendarShellInHost,
} from "./classes.js";
// ✦ Range selection (React `Calendar mode="range"`): start/end raised, days between on `bg-accent`.
let {
ref = $bindable(null),
value = $bindable(),
placeholder = $bindable(),
class: className,
weekdayFormat = "short",
captionLayout = "label",
locale = "en-US",
disableDaysOutsideMonth = false,
elevation = "auto",
...restProps
}: WithoutChildrenOrChild<RangeCalendarPrimitive.RootProps> & {
captionLayout?: "dropdown" | "dropdown-months" | "dropdown-years" | "label";
/** ✦ depth of the calendar shell: sunken -1, flat 0, raised +1, floating +2 (selected day rises when raised). */
elevation?: Elevation;
} = $props();
const shell = useElevation(() => elevation, "surface");
const handle = useElevation(() => (elevation === "sunken" ? "flat" : elevation), "handle");
const raised = $derived(handle.current === "raised" || handle.current === "floating");
</script>
<RangeCalendarPrimitive.Root
bind:value
bind:ref
bind:placeholder
{weekdayFormat}
{disableDaysOutsideMonth}
{locale}
data-slot="calendar"
class={cn(calendarRootClass, calendarShellElevation[shell.current], calendarShellInHost, className)}
{...restProps}
>
{#snippet children({ months, weekdays })}
<div class="relative flex flex-col gap-4 md:flex-row">
<nav
class="pointer-events-none absolute inset-x-0 top-0 flex h-7 w-full items-center justify-between gap-1"
>
<RangeCalendarPrimitive.PrevButton
data-slot="calendar-prev-button"
class={cn(buttonVariants({ variant: "outline" }), calendarNavButtonClass)}
>
<CaretLeftIcon
class="size-4"
/>
</RangeCalendarPrimitive.PrevButton>
<RangeCalendarPrimitive.NextButton
data-slot="calendar-next-button"
class={cn(buttonVariants({ variant: "outline" }), calendarNavButtonClass)}
>
<CaretRightIcon
class="size-4"
/>
</RangeCalendarPrimitive.NextButton>
</nav>
{#each months as month, monthIndex (month)}
<div class="flex w-full flex-col gap-3">
<RangeCalendarPrimitive.Header
data-slot="calendar-header"
class="flex h-7 w-full items-center justify-center gap-1.5 px-9"
>
<CalendarCaption
{captionLayout}
month={month.value}
bind:placeholder
{locale}
{monthIndex}
minValue={restProps.minValue}
maxValue={restProps.maxValue}
/>
</RangeCalendarPrimitive.Header>
<RangeCalendarPrimitive.Grid data-slot="calendar-grid" class="w-full border-collapse">
<RangeCalendarPrimitive.GridHead>
<RangeCalendarPrimitive.GridRow class="flex select-none">
{#each weekdays as weekday, i (i)}
<RangeCalendarPrimitive.HeadCell class={calendarHeadCellClass}>
{weekday.slice(0, 2)}
</RangeCalendarPrimitive.HeadCell>
{/each}
</RangeCalendarPrimitive.GridRow>
</RangeCalendarPrimitive.GridHead>
<RangeCalendarPrimitive.GridBody>
{#each month.weeks as weekDates (weekDates)}
<RangeCalendarPrimitive.GridRow class="flex w-full">
{#each weekDates as date (date)}
<RangeCalendarPrimitive.Cell
{date}
month={month.value}
data-slot="calendar-cell"
class={calendarRangeCellClass}
>
<RangeCalendarPrimitive.Day
data-slot="calendar-day"
class={cn(
buttonVariants({ variant: "ghost", size: "icon" }),
calendarDayClass,
calendarRangeMiddleClass,
calendarRangeEdgeClass,
raised && calendarRangeEdgeRaisedClass
)}
/>
</RangeCalendarPrimitive.Cell>
{/each}
</RangeCalendarPrimitive.GridRow>
{/each}
</RangeCalendarPrimitive.GridBody>
</RangeCalendarPrimitive.Grid>
</div>
{/each}
</div>
{/snippet}
</RangeCalendarPrimitive.Root>
src/lib/registry/ui/calendar/classes.ts
// Class strings shared by Calendar and RangeCalendar. Literal for Tailwind's scanner.
// Selected day (single, range start/end) is flat primary; the raised strings (v4 handle recipe) apply when the
// calendar elevation is raised or floating. The calendar SHELL takes the elevation, never the day grid.
export const calendarRootClass =
"group/calendar w-fit bg-card p-3 [--cell-radius:8px] [--cell-size:36px] in-data-[slot=card-content]:bg-transparent in-data-[slot=popover-content]:bg-transparent";
export const calendarNavButtonClass =
"pointer-events-auto size-7 rounded-md p-0 select-none disabled:opacity-50 rtl:rotate-180";
export const calendarHeadCellClass =
"flex h-7 flex-1 items-center justify-center text-[11.5px] font-normal text-muted-foreground select-none";
export const calendarCellClass =
"relative flex-1 p-0 text-center focus-within:relative focus-within:z-20";
export const calendarRangeCellClass =
"relative flex-1 p-0 text-center focus-within:relative focus-within:z-20 [&:has([data-selected])]:bg-accent [&:has([data-selection-start])]:rounded-l-(--cell-radius) [&:has([data-selection-end])]:rounded-r-(--cell-radius)";
export const calendarDayClass =
"relative isolate z-10 flex aspect-square size-auto w-full min-w-(--cell-size) flex-col items-center justify-center gap-1 rounded-(--cell-radius) border-0 text-[13px] leading-none font-normal whitespace-nowrap cursor-default select-none focus-visible:z-10 focus-visible:shadow-ring [&>span]:text-xs [&>span]:opacity-70 [&[data-today]:not([data-selected])]:bg-accent [&[data-today]:not([data-selected])]:font-semibold data-[disabled]:pointer-events-none data-[disabled]:text-muted-foreground data-[disabled]:opacity-50 data-[outside-month]:text-muted-foreground data-[outside-month]:opacity-50 data-[unavailable]:text-muted-foreground data-[unavailable]:line-through";
export const calendarSingleSelectedClass =
"data-[selected]:bg-primary data-[selected]:text-primary-foreground data-[selected]:hover:bg-primary data-[selected]:hover:text-primary-foreground";
export const calendarRangeEdgeClass =
"data-[selection-start]:bg-primary data-[selection-start]:text-primary-foreground data-[selection-start]:hover:bg-primary data-[selection-start]:hover:text-primary-foreground data-[selection-end]:bg-primary data-[selection-end]:text-primary-foreground data-[selection-end]:hover:bg-primary data-[selection-end]:hover:text-primary-foreground";
export const calendarShellElevation = {
sunken: "rounded-xl border border-sk-bd bg-sk-bg shadow-sunken",
flat: "",
raised: "rounded-xl border border-transparent shadow-raised",
floating: "rounded-xl border border-transparent shadow-floating",
};
// popover / card hosts own the shell: the calendar drops its own edge there
export const calendarShellInHost =
"in-data-[slot=popover-content]:border-0 in-data-[slot=popover-content]:shadow-none";
// handle recipe: the selected day rises
export const calendarSingleSelectedRaisedClass =
"data-[selected]:border-transparent data-[selected]:[background-image:var(--r1-p-face)] data-[selected]:shadow-btn-raised-primary";
export const calendarRangeEdgeRaisedClass =
"data-[selection-start]:border-transparent data-[selection-start]:[background-image:var(--r1-p-face)] data-[selection-start]:shadow-btn-raised-primary data-[selection-end]:border-transparent data-[selection-end]:[background-image:var(--r1-p-face)] data-[selection-end]:shadow-btn-raised-primary";
export const calendarRangeMiddleClass =
"[&[data-selected]:not([data-selection-start]):not([data-selection-end])]:rounded-none [&[data-selected]:not([data-selection-start]):not([data-selection-end])]:bg-accent [&[data-selected]:not([data-selection-start]):not([data-selection-end])]:text-foreground data-[highlighted]:rounded-none data-[highlighted]:bg-accent";
src/lib/registry/ui/calendar/index.ts
import Root from "./calendar.svelte";
import Caption from "./calendar-caption.svelte";
import RangeCalendar from "./range-calendar.svelte";
export {
Caption,
RangeCalendar,
//
Root as Calendar,
};
StackBlitz ↗StackBlitz ↗StackBlitz ↗
October 2026
Event Date, October 2026
October 2026
27
28
29
30
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
October 2026
SuMoTuWeThFrSa
27
28
29
30
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
StackBlitz ↗StackBlitz ↗StackBlitz ↗
October 2026
Oct
2026
27
28
29
30
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
Event Date, October 2026
SuMoTuWeThFrSa
27
28
29
30
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

See the Elevation guide for the levels, roles and layered mode.

elevation sits on the calendar shell (sunken -1, flat, raised +1, floating +2), never on the day grid. When raised or floating the selected day (and range ends) rise with the handle recipe. Inside a popover or card the host owns the shell, so only the selected day changes.

StackBlitz ↗StackBlitz ↗StackBlitz ↗

Sunken (-1)

October 2026

Flat (0)

October 2026

Raised (+1)

October 2026

Floating (+2)

October 2026

Sunken (-1)

October 2026
27
28
29
30
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
Event Date, October 2026

Flat (0)

October 2026
27
28
29
30
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
Event Date, October 2026

Raised (+1)

October 2026
27
28
29
30
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
Event Date, October 2026

Floating (+2)

October 2026
27
28
29
30
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
Event Date, October 2026

Sunken (-1)

October 2026
SuMoTuWeThFrSa
27
28
29
30
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

Flat (0)

October 2026
SuMoTuWeThFrSa
27
28
29
30
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

Raised (+1)

October 2026
SuMoTuWeThFrSa
27
28
29
30
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

Floating (+2)

October 2026
SuMoTuWeThFrSa
27
28
29
30
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

Built on react-day-picker v10 (and date-fns). All DayPicker props are forwarded.

Prop Type Default
mode "single" | "multiple" | "range" -
selected / onSelect per mode -
captionLayout "label" | "dropdown" | "dropdown-months" | "dropdown-years" "label"
showOutsideDays boolean true
showWeekNumber boolean false
buttonVariant Button variant "outline"
--cell-size (CSS var) length 36px
elevation "auto" | "sunken" | "flat" | "raised" | "floating" "auto"

Exports: Calendar, CalendarDayButton.

  • Selected day and range endpoints are flat primary fills (elevation raised: bevel); range middle is a flat --accent band.
  • Month/year dropdowns are bordered mini-buttons with a soft focus ring; nav buttons use the outline button recipe.
  • Vue and Svelte ship RangeCalendar as a separate component. React selects a range with mode="range" on Calendar.