Calendar
Month grid for picking a date or a range. Today is tinted; the selected day is a primary fill (elevation raises it).
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
import { Calendar } from "@/components/ui/calendar";import * as React from "react";
export default function Demo() { const [date, setDate] = React.useState<Date | undefined>( new Date(2026, 9, 16), ); return ( <Calendar mode="single" selected={date} onSelect={setDate} defaultMonth={date} className="rounded-xl border border-border" /> );}<script setup lang="ts">import { CalendarDate } from "@internationalized/date";import { Calendar } from "@/components/ui/calendar";import { ref } from "vue";
const date = ref(new CalendarDate(2026, 10, 16));</script>
<template> <Calendar v-model="date" class="rounded-xl border border-border" /></template><script lang="ts"> import { CalendarDate } from "@internationalized/date"; import { Calendar } from "$lib/components/ui/calendar";
let value = $state(new CalendarDate(2026, 10, 16));</script>
<Calendar type="single" bind:value class="rounded-xl border border-border" />Installation
Section titled “Installation”npx shadcn@latest add @edmi-ui/calendarpnpm dlx shadcn@latest add @edmi-ui/calendaryarn dlx shadcn@latest add @edmi-ui/calendarbunx shadcn@latest add @edmi-ui/calendarPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Install the dependencies:
Terminal window npm install @base-ui/react date-fns react-day-pickerTerminal window pnpm add @base-ui/react date-fns react-day-pickerTerminal window yarn add @base-ui/react date-fns react-day-pickerTerminal window bun add @base-ui/react date-fns react-day-pickerAdd the Edmi components this one builds on:
button,elevation.Copy the code from Component source into your project and update the import paths to match your aliases.
npx shadcn-vue@latest add @edmi-ui/calendarpnpm dlx shadcn-vue@latest add @edmi-ui/calendaryarn dlx shadcn-vue@latest add @edmi-ui/calendarbunx shadcn-vue@latest add @edmi-ui/calendarPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Install the dependencies:
Terminal window npm install reka-ui @vueuse/core @internationalized/dateTerminal window pnpm add reka-ui @vueuse/core @internationalized/dateTerminal window yarn add reka-ui @vueuse/core @internationalized/dateTerminal window bun add reka-ui @vueuse/core @internationalized/dateAdd the Edmi components this one builds on:
button,elevation.Copy the code from Component source into your project and update the import paths to match your aliases.
npx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/calendar.jsonpnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/calendar.jsonyarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/calendar.jsonbunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/calendar.jsonPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Add the Edmi components this one builds on:
button,elevation.Copy the code from Component source into your project and update the import paths to match your aliases.
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" />Component source
Section titled “Component source”"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 };<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><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><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>// 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 thereexport 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";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;<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 notget 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><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}<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>// 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 thereexport const calendarShellInHost = "in-data-[slot=popover-content]:border-0 in-data-[slot=popover-content]:shadow-none";
// handle recipe: the selected day risesexport 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";import Root from "./calendar.svelte";import Caption from "./calendar-caption.svelte";import RangeCalendar from "./range-calendar.svelte";
export { Caption, RangeCalendar, // Root as Calendar,};| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
import { Calendar } from "@/components/ui/calendar";import * as React from "react";import type { DateRange } from "react-day-picker";
export default function Demo() { const [range, setRange] = React.useState<DateRange | undefined>({ from: new Date(2026, 9, 12), to: new Date(2026, 9, 18), }); return ( <Calendar mode="range" selected={range} onSelect={setRange} defaultMonth={range?.from} className="rounded-xl border border-border" /> );}<script setup lang="ts">import { CalendarDate } from "@internationalized/date";import { RangeCalendar } from "@/components/ui/calendar";import { ref } from "vue";
const range = ref({ start: new CalendarDate(2026, 10, 12), end: new CalendarDate(2026, 10, 18) });</script>
<template> <RangeCalendar v-model="range" class="rounded-xl border border-border" /></template><script lang="ts"> import { CalendarDate } from "@internationalized/date"; import { RangeCalendar } from "$lib/components/ui/calendar";
let value = $state({ start: new CalendarDate(2026, 10, 12), end: new CalendarDate(2026, 10, 18) });</script>
<RangeCalendar bind:value class="rounded-xl border border-border" />Dropdown caption
Section titled “Dropdown caption”| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
import { Calendar } from "@/components/ui/calendar";import * as React from "react";
export default function Demo() { const [date, setDate] = React.useState<Date | undefined>( new Date(2026, 9, 16), ); return ( <Calendar mode="single" captionLayout="dropdown" selected={date} onSelect={setDate} defaultMonth={date} startMonth={new Date(2020, 0)} endMonth={new Date(2030, 11)} className="rounded-xl border border-border" /> );}<script setup lang="ts">import { CalendarDate } from "@internationalized/date";import { Calendar } from "@/components/ui/calendar";import { ref } from "vue";
const date = ref(new CalendarDate(2026, 10, 16));</script>
<template> <Calendar v-model="date" layout="month-and-year" :min-value="new CalendarDate(2020, 1, 1)" :max-value="new CalendarDate(2030, 12, 31)" class="rounded-xl border border-border" /></template><script lang="ts"> import { CalendarDate } from "@internationalized/date"; import { Calendar } from "$lib/components/ui/calendar";
let value = $state(new CalendarDate(2026, 10, 16));</script>
<Calendar type="single" bind:value captionLayout="dropdown" minValue={new CalendarDate(2020, 1, 1)} maxValue={new CalendarDate(2030, 12, 31)} class="rounded-xl border border-border"/>Elevation ✦
Section titled “Elevation ✦”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.
Sunken (-1)
Flat (0)
Raised (+1)
Floating (+2)
Sunken (-1)
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
Flat (0)
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
Raised (+1)
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
Floating (+2)
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
Sunken (-1)
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
Flat (0)
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
Raised (+1)
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
Floating (+2)
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
import { Calendar } from "@/components/ui/calendar";import { useState } from "react";
const levels = [ { value: "sunken", label: "Sunken (-1)" }, { value: "flat", label: "Flat (0)" }, { value: "raised", label: "Raised (+1)" }, { value: "floating", label: "Floating (+2)" },] as const;
function Example({ elevation,}: { elevation: "sunken" | "flat" | "raised" | "floating";}) { const [date, setDate] = useState<Date | undefined>(new Date(2026, 9, 16)); return ( <Calendar elevation={elevation} mode="single" selected={date} onSelect={setDate} defaultMonth={date} /> );}
export default function Demo() { return ( <div className="flex flex-col gap-6"> {levels.map(({ value, label }) => ( <div key={value} className="flex flex-col gap-2"> <p className="text-xs font-medium text-muted-foreground">{label}</p> <Example elevation={value} /> </div> ))} </div> );}<script setup lang="ts">import { getLocalTimeZone, today } from "@internationalized/date";import { Calendar } from "@/components/ui/calendar";import { ref } from "vue";
const levels = [{ value: "sunken", label: "Sunken (-1)" }, { value: "flat", label: "Flat (0)" }, { value: "raised", label: "Raised (+1)" }, { value: "floating", label: "Floating (+2)" }];
const date = ref(today(getLocalTimeZone()));</script>
<template> <div class="flex flex-col gap-6"> <div v-for="level in levels" :key="level.value" class="flex flex-col gap-2"> <p class="text-xs font-medium text-muted-foreground">{{ level.label }}</p> <Calendar v-model="date" :elevation="level.value" /> </div> </div></template><script lang="ts"> import { CalendarDate } from "@internationalized/date"; import { Calendar } from "$lib/components/ui/calendar";
let day = $state(new CalendarDate(2026, 10, 16));
const levels = [{ value: "sunken", label: "Sunken (-1)" }, { value: "flat", label: "Flat (0)" }, { value: "raised", label: "Raised (+1)" }, { value: "floating", label: "Floating (+2)" }] as const;</script>
<div class="flex flex-col gap-6"> {#each levels as level (level.value)} <div class="flex flex-col gap-2"> <p class="text-xs font-medium text-muted-foreground">{level.label}</p> <Calendar type="single" elevation={level.value} bind:value={day} /> </div> {/each}</div>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.
✦ Edmi additions
Section titled “✦ Edmi additions”- Selected day and range endpoints are flat primary fills (
elevationraised: bevel); range middle is a flat--accentband. - Month/year dropdowns are bordered mini-buttons with a soft focus ring; nav buttons use the outline button recipe.
Vue / Svelte differences
Section titled “Vue / Svelte differences”- Vue and Svelte ship
RangeCalendaras a separate component. React selects a range withmode="range"onCalendar.