Date Picker
A button that opens a calendar in a popover. Ranges can offer presets on the side.
import { DatePicker } from "@/components/date-picker/date-picker";
export default function Demo() { return <DatePicker defaultValue={new Date(2026, 9, 16)} />;}<script setup lang="ts">import { CalendarDate } from "@internationalized/date";import { DatePicker } from "@/components/ui/date-picker";</script>
<template> <DatePicker :default-value="new CalendarDate(2026, 10, 16)" /></template><script lang="ts"> import { CalendarDate } from "@internationalized/date"; import { DatePicker } from "$lib/components/ui/date-picker";
let value = $state(new CalendarDate(2026, 10, 16));</script>
<DatePicker bind:value />Installation
Section titled “Installation”npx shadcn@latest add @edmi-ui/date-pickerpnpm dlx shadcn@latest add @edmi-ui/date-pickeryarn dlx shadcn@latest add @edmi-ui/date-pickerbunx shadcn@latest add @edmi-ui/date-pickerPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Install the dependencies:
Terminal window npm install date-fns react-day-pickerTerminal window pnpm add date-fns react-day-pickerTerminal window yarn add date-fns react-day-pickerTerminal window bun add date-fns react-day-pickerAdd the Edmi components this one builds on:
button,calendar,popover,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/date-pickerpnpm dlx shadcn-vue@latest add @edmi-ui/date-pickeryarn dlx shadcn-vue@latest add @edmi-ui/date-pickerbunx shadcn-vue@latest add @edmi-ui/date-pickerPick 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,calendar,popover,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/date-picker.jsonpnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/date-picker.jsonyarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/date-picker.jsonbunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/date-picker.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,calendar,popover,elevation.Copy the code from Component source into your project and update the import paths to match your aliases.
import { DatePicker } from "@/components/date-picker/date-picker";
<DatePicker defaultValue={new Date(2026, 9, 16)} /><script setup lang="ts">import { DatePicker } from "@/components/ui/date-picker";</script>
<template> <DatePicker :default-value="new CalendarDate(2026, 10, 16)" /></template><script lang="ts"> import { DatePicker } from "$lib/components/ui/date-picker";</script>
<DatePicker bind:value />Component source
Section titled “Component source”import { cn } from "cn";import { endOfMonth, format, startOfMonth, subDays } from "date-fns";import * as React from "react";import type { DateRange } from "react-day-picker";import { CalendarBlankIcon } from "@phosphor-icons/react";
import { Button } from "@/registry/edmi/ui/button";import { Calendar } from "@/registry/edmi/ui/calendar";import type { Elevation } from "@/registry/edmi/ui/elevation";import { Popover, PopoverContent, PopoverTrigger,} from "@/registry/edmi/ui/popover";
// Date Picker = composition, not a component: Popover + outline Button + Calendar.
type TriggerProps = Omit<React.ComponentProps<typeof Button>, "value">;
function DatePicker({ value, defaultValue, onValueChange, placeholder = "Pick a date", elevation, className, ...props}: Omit<TriggerProps, "defaultValue" | "onChange"> & { value?: Date; defaultValue?: Date; onValueChange?: (date: Date | undefined) => void; placeholder?: string; /** ✦ forwarded to the trigger Button and the Calendar */ elevation?: Elevation;}) { const [inner, setInner] = React.useState<Date | undefined>(defaultValue); const [open, setOpen] = React.useState(false); const date = value ?? inner; return ( <Popover open={open} onOpenChange={setOpen}> <PopoverTrigger render={ <Button variant="outline" elevation={elevation} data-empty={!date} className={cn( "w-[240px] justify-start text-left font-normal data-[empty=true]:text-muted-foreground", className, )} {...props} /> } > <CalendarBlankIcon /> {date ? format(date, "PPP") : <span>{placeholder}</span>} </PopoverTrigger> <PopoverContent className="w-auto p-0" align="start"> <Calendar mode="single" elevation={elevation} captionLayout="dropdown" selected={date} defaultMonth={date} onSelect={(next) => { setInner(next); onValueChange?.(next); setOpen(false); }} /> </PopoverContent> </Popover> );}
// ✦ Ranges can offer presets in a column to the left of the calendar.type Preset = { label: string; range: () => DateRange };
const defaultPresets: Preset[] = [ { label: "Today", range: () => ({ from: new Date(), to: new Date() }) }, { label: "Last 7 days", range: () => ({ from: subDays(new Date(), 6), to: new Date() }), }, { label: "Last 30 days", range: () => ({ from: subDays(new Date(), 29), to: new Date() }), }, { label: "This month", range: () => ({ from: startOfMonth(new Date()), to: endOfMonth(new Date()), }), },];
function formatRange(range: DateRange | undefined, placeholder: string) { if (!range?.from) return <span>{placeholder}</span>; if (!range.to) return format(range.from, "LLL dd, y"); return `${format(range.from, "LLL dd, y")} – ${format(range.to, "LLL dd, y")}`;}
function DateRangePicker({ value, defaultValue, onValueChange, placeholder = "Pick a date range", presets, elevation, className, ...props}: Omit<TriggerProps, "defaultValue" | "onChange"> & { value?: DateRange; defaultValue?: DateRange; onValueChange?: (range: DateRange | undefined) => void; placeholder?: string; /** ✦ `true` for the default presets, or your own list. */ presets?: boolean | Preset[]; /** ✦ forwarded to the trigger Button and the Calendar */ elevation?: Elevation;}) { const [inner, setInner] = React.useState<DateRange | undefined>(defaultValue); const range = value ?? inner; const list = presets === true ? defaultPresets : presets || []; const set = (next: DateRange | undefined) => { setInner(next); onValueChange?.(next); }; return ( <Popover> <PopoverTrigger render={ <Button variant="outline" elevation={elevation} data-empty={!range?.from} className={cn( "w-[260px] justify-start text-left font-normal data-[empty=true]:text-muted-foreground", className, )} {...props} /> } > <CalendarBlankIcon /> {formatRange(range, placeholder)} </PopoverTrigger> <PopoverContent className="w-auto flex-row gap-0 p-0" align="start"> {list.length > 0 && ( <div className="flex w-36 flex-col gap-0.5 border-r border-border p-1.5"> {list.map((preset) => ( <Button key={preset.label} variant="ghost" size="sm" className="justify-start font-normal" onClick={() => set(preset.range())} > {preset.label} </Button> ))} </div> )} <Calendar mode="range" elevation={elevation} numberOfMonths={1} selected={range} defaultMonth={range?.from} onSelect={set} /> </PopoverContent> </Popover> );}
export type { Preset as DatePickerPreset };export { DatePicker, DateRangePicker };<script setup lang="ts">import type { DateValue } from "reka-ui"import type { HTMLAttributes, Ref } from "vue"import { DateFormatter, getLocalTimeZone } from "@internationalized/date"import { CalendarIcon } from "@lucide/vue"import { useVModel } from "@vueuse/core"import { computed, ref } from "vue"import { cn } from "@/registry/edmi/lib/utils"import { Button } from "@/registry/edmi/ui/button"import type { Elevation } from "@/registry/edmi/ui/elevation"import { Calendar } from "@/registry/edmi/ui/calendar"import { Popover, PopoverContent, PopoverTrigger } from "@/registry/edmi/ui/popover"
// Date Picker = composition, not a component: Popover + outline Button + Calendar.const props = withDefaults( defineProps<{ modelValue?: DateValue defaultValue?: DateValue placeholder?: string locale?: string disabled?: boolean /** ✦ forwarded to the trigger Button and the Calendar */ elevation?: Elevation class?: HTMLAttributes["class"] }>(), { placeholder: "Pick a date", locale: "en-US", elevation: undefined },)const emits = defineEmits<{ (e: "update:modelValue", value: DateValue | undefined): void }>()
const date = useVModel(props, "modelValue", emits, { passive: true, defaultValue: props.defaultValue,}) as Ref<DateValue | undefined>
const open = ref(false)
const formatter = computed(() => new DateFormatter(props.locale, { dateStyle: "long" }))</script>
<template> <Popover v-model:open="open"> <PopoverTrigger as-child> <Button variant="outline" :elevation="elevation" :disabled="disabled" :data-empty="!date" :class="cn('w-[240px] justify-start text-left font-normal data-[empty=true]:text-muted-foreground', props.class)" > <CalendarIcon /> {{ date ? formatter.format(date.toDate(getLocalTimeZone())) : placeholder }} </Button> </PopoverTrigger> <PopoverContent class="w-auto p-0" align="start"> <Calendar v-model="date" :elevation="elevation" layout="month-and-year" :locale="locale" :default-placeholder="date" @update:model-value="open = false" /> </PopoverContent> </Popover></template><script setup lang="ts">import type { DateValue } from "reka-ui"import type { DateRangePickerPreset } from "."import type { HTMLAttributes, Ref } from "vue"import { DateFormatter, endOfMonth, getLocalTimeZone, startOfMonth, today } from "@internationalized/date"import { CalendarIcon } from "@lucide/vue"import { useVModel } from "@vueuse/core"import { computed } from "vue"import { cn } from "@/registry/edmi/lib/utils"import { Button } from "@/registry/edmi/ui/button"import type { Elevation } from "@/registry/edmi/ui/elevation"import { RangeCalendar } from "@/registry/edmi/ui/calendar"import { Popover, PopoverContent, PopoverTrigger } from "@/registry/edmi/ui/popover"
type DateRange = { start: DateValue | undefined; end: DateValue | undefined }
const props = withDefaults( defineProps<{ modelValue?: DateRange defaultValue?: DateRange placeholder?: string locale?: string disabled?: boolean /** ✦ `true` for the default presets, or your own list. */ presets?: boolean | DateRangePickerPreset[] /** ✦ forwarded to the trigger Button and the RangeCalendar */ elevation?: Elevation class?: HTMLAttributes["class"] }>(), { placeholder: "Pick a date range", locale: "en-US", elevation: undefined },)const emits = defineEmits<{ (e: "update:modelValue", value: DateRange | undefined): void }>()
const range = useVModel(props, "modelValue", emits, { passive: true, defaultValue: props.defaultValue,}) as Ref<DateRange | undefined>
const tz = getLocalTimeZone()const defaultPresets: DateRangePickerPreset[] = [ { label: "Today", range: () => ({ start: today(tz), end: today(tz) }) }, { label: "Last 7 days", range: () => ({ start: today(tz).subtract({ days: 6 }), end: today(tz) }) }, { label: "Last 30 days", range: () => ({ start: today(tz).subtract({ days: 29 }), end: today(tz) }) }, { label: "This month", range: () => ({ start: startOfMonth(today(tz)), end: endOfMonth(today(tz)) }) },]const list = computed(() => (props.presets === true ? defaultPresets : props.presets || []))
const formatter = computed(() => new DateFormatter(props.locale, { dateStyle: "medium" }))const label = computed(() => { const r = range.value if (!r?.start) return props.placeholder const from = formatter.value.format(r.start.toDate(tz)) if (!r.end) return from return `${from} – ${formatter.value.format(r.end.toDate(tz))}`})</script>
<template> <Popover> <PopoverTrigger as-child> <Button variant="outline" :elevation="elevation" :disabled="disabled" :data-empty="!range?.start" :class="cn('w-[260px] justify-start text-left font-normal data-[empty=true]:text-muted-foreground', props.class)" > <CalendarIcon /> {{ label }} </Button> </PopoverTrigger> <PopoverContent class="w-auto flex-row gap-0 p-0" align="start"> <div v-if="list.length > 0" class="flex w-36 flex-col gap-0.5 border-r border-border p-1.5"> <Button v-for="preset in list" :key="preset.label" variant="ghost" size="sm" class="justify-start font-normal" @click="range = preset.range()" > {{ preset.label }} </Button> </div> <RangeCalendar v-model="range as any" :elevation="elevation" layout="month-and-year" :locale="locale" :number-of-months="1" :default-placeholder="range?.start" /> </PopoverContent> </Popover></template>import type { DateValue } from "reka-ui";
export { default as DatePicker } from "./DatePicker.vue";export { default as DateRangePicker } from "./DateRangePicker.vue";
/** ✦ A quick range shown in a column left of the calendar. */export type DateRangePickerPreset = { label: string; range: () => { start: DateValue | undefined; end: DateValue | undefined };};<script lang="ts"> import { DateFormatter, getLocalTimeZone, type DateValue } from "@internationalized/date"; import CalendarBlankIcon from "phosphor-svelte/lib/CalendarBlank"; import { cn } from "$lib/utils.js"; import { Button } from "$lib/registry/ui/button/index.js"; import type { Elevation } from "$lib/registry/ui/elevation/index.js"; import { Calendar } from "$lib/registry/ui/calendar/index.js"; import * as Popover from "$lib/registry/ui/popover/index.js";
// Date Picker = composition, not a component: Popover + outline Button + Calendar. let { value = $bindable(), placeholder = "Pick a date", locale = "en-US", disabled = false, elevation = "auto", class: className, }: { value?: DateValue; placeholder?: string; locale?: string; disabled?: boolean; /** ✦ depth; forwarded to the trigger button and the calendar. */ elevation?: Elevation; class?: string; } = $props();
let open = $state(false);
const formatter = $derived(new DateFormatter(locale, { dateStyle: "long" }));</script>
<Popover.Root bind:open> <Popover.Trigger> {#snippet child({ props })} <Button variant="outline" {elevation} {disabled} data-empty={!value} class={cn( "w-[240px] justify-start text-left font-normal data-[empty=true]:text-muted-foreground", className )} {...props} > <CalendarBlankIcon /> {value ? formatter.format(value.toDate(getLocalTimeZone())) : placeholder} </Button> {/snippet} </Popover.Trigger> <Popover.Content class="w-auto p-0" align="start"> <Calendar type="single" bind:value captionLayout="dropdown" {locale} {elevation} onValueChange={() => (open = false)} /> </Popover.Content></Popover.Root><script lang="ts" module> import type { DateValue } from "@internationalized/date";
type DateRange = { start: DateValue | undefined; end: DateValue | undefined }; /** ✦ A quick range shown in a column left of the calendar. */ export type DateRangePickerPreset = { label: string; range: () => DateRange };</script>
<script lang="ts"> import { DateFormatter, endOfMonth, getLocalTimeZone, startOfMonth, today, } from "@internationalized/date"; import CalendarBlankIcon from "phosphor-svelte/lib/CalendarBlank"; import { cn } from "$lib/utils.js"; import { Button } from "$lib/registry/ui/button/index.js"; import type { Elevation } from "$lib/registry/ui/elevation/index.js"; import { RangeCalendar } from "$lib/registry/ui/calendar/index.js"; import * as Popover from "$lib/registry/ui/popover/index.js";
let { value = $bindable(), placeholder = "Pick a date range", locale = "en-US", disabled = false, presets, elevation = "auto", class: className, }: { value?: DateRange; placeholder?: string; locale?: string; disabled?: boolean; /** ✦ `true` for the default presets, or your own list. */ presets?: boolean | DateRangePickerPreset[]; /** ✦ depth; forwarded to the trigger button and the calendar. */ elevation?: Elevation; class?: string; } = $props();
const tz = getLocalTimeZone(); const defaultPresets: DateRangePickerPreset[] = [ { label: "Today", range: () => ({ start: today(tz), end: today(tz) }) }, { label: "Last 7 days", range: () => ({ start: today(tz).subtract({ days: 6 }), end: today(tz) }), }, { label: "Last 30 days", range: () => ({ start: today(tz).subtract({ days: 29 }), end: today(tz) }), }, { label: "This month", range: () => ({ start: startOfMonth(today(tz)), end: endOfMonth(today(tz)) }), }, ]; const list = $derived(presets === true ? defaultPresets : presets || []);
const formatter = $derived(new DateFormatter(locale, { dateStyle: "medium" })); const label = $derived.by(() => { if (!value?.start) return placeholder; const from = formatter.format(value.start.toDate(tz)); if (!value.end) return from; return `${from} – ${formatter.format(value.end.toDate(tz))}`; });</script>
<Popover.Root> <Popover.Trigger> {#snippet child({ props })} <Button variant="outline" {elevation} {disabled} data-empty={!value?.start} class={cn( "w-[260px] justify-start text-left font-normal data-[empty=true]:text-muted-foreground", className )} {...props} > <CalendarBlankIcon /> {label} </Button> {/snippet} </Popover.Trigger> <Popover.Content class="w-auto flex-row gap-0 p-0" align="start"> {#if list.length > 0} <div class="flex w-36 flex-col gap-0.5 border-r border-border p-1.5"> {#each list as preset (preset.label)} <Button variant="ghost" size="sm" class="justify-start font-normal" onclick={() => (value = preset.range())} > {preset.label} </Button> {/each} </div> {/if} <RangeCalendar bind:value numberOfMonths={1} captionLayout="dropdown" {locale} {elevation} /> </Popover.Content></Popover.Root>import DatePicker from "./date-picker.svelte";import DateRangePicker from "./date-range-picker.svelte";
export type { DateRangePickerPreset } from "./date-range-picker.svelte";export { DatePicker, DateRangePicker };Range with presets
Section titled “Range with presets”import { DateRangePicker } from "@/components/date-picker/date-picker";
export default function Demo() { return ( <DateRangePicker presets defaultValue={{ from: new Date(2026, 9, 12), to: new Date(2026, 9, 18) }} /> );}<script setup lang="ts">import { CalendarDate } from "@internationalized/date";import { DateRangePicker } from "@/components/ui/date-picker";</script>
<template> <DateRangePicker presets :default-value="{ start: new CalendarDate(2026, 10, 12), end: new CalendarDate(2026, 10, 18) }" /></template><script lang="ts"> import { CalendarDate } from "@internationalized/date"; import { DateRangePicker } from "$lib/components/ui/date-picker";
let value = $state({ start: new CalendarDate(2026, 10, 12), end: new CalendarDate(2026, 10, 18) });</script>
<DateRangePicker bind:value presets />Elevation ✦
Section titled “Elevation ✦”See the Elevation guide for the levels, roles and layered mode.
elevation is forwarded to the trigger button and the calendar (the selected day rises when raised or floating); the popover stays floating.
Flat (0)
Raised (+1)
Floating (+2)
Flat (0)
Raised (+1)
Floating (+2)
Flat (0)
Raised (+1)
Floating (+2)
import { DatePicker } from "@/components/date-picker/date-picker";
const levels = [ { value: "flat", label: "Flat (0)" }, { value: "raised", label: "Raised (+1)" }, { value: "floating", label: "Floating (+2)" },] as const;
export default function Demo() { return ( <div className="flex flex-col gap-5"> {levels.map(({ value, label }) => ( <div key={value} className="flex flex-col gap-2"> <p className="text-xs font-medium text-muted-foreground">{label}</p> <DatePicker elevation={value} defaultValue={new Date(2026, 9, 16)} /> </div> ))} </div> );}<script setup lang="ts">import { CalendarDate } from "@internationalized/date";import { DatePicker } from "@/components/ui/date-picker";
const levels = [{ value: "flat", label: "Flat (0)" }, { value: "raised", label: "Raised (+1)" }, { value: "floating", label: "Floating (+2)" }];</script>
<template> <div class="flex flex-col gap-5"> <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> <DatePicker :elevation="level.value" :default-value="new CalendarDate(2026, 10, 16)" /> </div> </div></template><script lang="ts"> import { CalendarDate } from "@internationalized/date"; import { DatePicker } from "$lib/components/ui/date-picker";
let date = $state(new CalendarDate(2026, 10, 16));
const levels = [{ value: "flat", label: "Flat (0)" }, { value: "raised", label: "Raised (+1)" }, { value: "floating", label: "Floating (+2)" }] as const;</script>
<div class="flex flex-col gap-5"> {#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> <DatePicker elevation={level.value} bind:value={date} /> </div> {/each}</div>Shipped as a registry:block (shadcn documents it as a composition): Popover + outline Button + Calendar.
| Component | Prop | Type | Default |
|---|---|---|---|
DatePicker |
value / defaultValue |
Date |
- |
onValueChange |
(date?: Date) => void |
- | |
placeholder |
string |
"Pick a date" |
|
elevation ✦ |
"auto" | "sunken" | "flat" | "raised" | "floating" |
"auto" |
|
DateRangePicker ✦ |
value / defaultValue |
DateRange |
- |
onValueChange |
(range?: DateRange) => void |
- | |
presets |
boolean | { label; range() }[] |
- | |
elevation ✦ |
"auto" | "sunken" | "flat" | "raised" | "floating" |
"auto" |
✦ Edmi additions
Section titled “✦ Edmi additions”DateRangePickerwith an optional preset column (Today, Last 7 days, Last 30 days, This month).DatePickeruses the dropdown caption layout, as on the board.
Vue / Svelte differences
Section titled “Vue / Svelte differences”- Vue and Svelte: Date Picker is a
registry:uiitem (a plain component that installs intocomponents/ui/date-picker), not a block.