Skip to content
edmi-ui

Date Picker

A button that opens a calendar in a popover. Ranges can offer presets on the side.

StackBlitz ↗StackBlitz ↗StackBlitz ↗
Terminal window
npx shadcn@latest add @edmi-ui/date-picker
Terminal window
pnpm dlx shadcn@latest add @edmi-ui/date-picker
Terminal window
yarn dlx shadcn@latest add @edmi-ui/date-picker
Terminal window
bunx shadcn@latest add @edmi-ui/date-picker

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/date-picker
Terminal window
pnpm dlx shadcn-vue@latest add @edmi-ui/date-picker
Terminal window
yarn dlx shadcn-vue@latest add @edmi-ui/date-picker
Terminal window
bunx shadcn-vue@latest add @edmi-ui/date-picker

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/date-picker.json
Terminal window
pnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/date-picker.json
Terminal window
yarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/date-picker.json
Terminal window
bunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/date-picker.json

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

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 />
registry/blocks/date-picker/date-picker.tsx
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 };
registry/ui/date-picker/DatePicker.vue
<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>
registry/ui/date-picker/DateRangePicker.vue
<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>
registry/ui/date-picker/index.ts
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 };
};
src/lib/registry/ui/date-picker/date-picker.svelte
<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>
src/lib/registry/ui/date-picker/date-range-picker.svelte
<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>
src/lib/registry/ui/date-picker/index.ts
import DatePicker from "./date-picker.svelte";
import DateRangePicker from "./date-range-picker.svelte";
export type { DateRangePickerPreset } from "./date-range-picker.svelte";
export { DatePicker, DateRangePicker };
StackBlitz ↗StackBlitz ↗StackBlitz ↗

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.

StackBlitz ↗StackBlitz ↗StackBlitz ↗

Flat (0)

Raised (+1)

Floating (+2)

Flat (0)

Raised (+1)

Floating (+2)

Flat (0)

Raised (+1)

Floating (+2)

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"
  • DateRangePicker with an optional preset column (Today, Last 7 days, Last 30 days, This month).
  • DatePicker uses the dropdown caption layout, as on the board.
  • Vue and Svelte: Date Picker is a registry:ui item (a plain component that installs into components/ui/date-picker), not a block.