Date range picker
A period, picked across one or two calendars.
'use client'
import { DateRangePicker, type DateRangeValue } from '@/components/date-range-picker'
import { useState } from 'react'
export default function Example() {
let [range, setRange] = useState<DateRangeValue>(null)
return <DateRangePicker value={range} onChange={setRange} />
}<script setup lang="ts">
import DateRangePicker, { type DateRangeValue } from '@/components/date-range-picker.vue'
import { ref } from 'vue'
const range = ref<DateRangeValue>(null)
</script>
<template>
<DateRangePicker v-model="range" />
</template>Dependencies
Same calendar as the date picker, so the same package:
npm install react-day-pickerpnpm add react-day-pickeryarn add react-day-pickerbun add react-day-pickernpm install reka-ui @internationalized/datepnpm add reka-ui @internationalized/dateyarn add reka-ui @internationalized/datebun add reka-ui @internationalized/dateThis component reuses the calendar styling exported by date-picker.tsx, so copy that file across too.
Installation
Copy these files into the folder where you keep your components. They import each other with relative paths, so the folder can live anywhere. The packages they rely on are listed on the Installation page.
'use client'
import * as Headless from '@headlessui/react'
import { Popover, PopoverPanel } from '@headlessui/react'
import { CalendarIcon, ChevronDownIcon } from '@heroicons/react/16/solid'
import clsx from 'clsx'
import type React from 'react'
import { useId, useState } from 'react'
import { DayPicker, type DateRange } from 'react-day-picker'
import { calendarComponents, fromISO, PickerButton, toISO, useCalendarLocale } from './date-picker'
const calendarClassNames = {
root: 'relative',
months: 'relative flex flex-col gap-4 md:flex-row',
month: 'flex flex-col',
nav: 'pointer-events-none absolute inset-x-0 top-0 z-10 flex h-9 items-center justify-between px-1',
month_caption: 'flex h-9 items-center justify-center px-8',
dropdowns: 'flex min-w-0 items-center gap-1',
caption_label:
'flex items-center gap-0.5 px-2 py-1 text-sm/6 font-medium text-zinc-950 select-none dark:text-white [&>svg]:size-3.5 [&>svg]:text-zinc-500 dark:[&>svg]:text-zinc-400',
button_previous:
'pointer-events-auto inline-flex size-7 shrink-0 cursor-pointer items-center justify-center rounded-md text-zinc-500 hover:bg-zinc-950/5 hover:text-zinc-950 focus:outline-hidden focus-visible:outline-2 focus-visible:outline-accent-500 disabled:opacity-30 dark:text-zinc-400 dark:hover:bg-white/5 dark:hover:text-white',
button_next:
'pointer-events-auto inline-flex size-7 shrink-0 cursor-pointer items-center justify-center rounded-md text-zinc-500 hover:bg-zinc-950/5 hover:text-zinc-950 focus:outline-hidden focus-visible:outline-2 focus-visible:outline-accent-500 disabled:opacity-30 dark:text-zinc-400 dark:hover:bg-white/5 dark:hover:text-white',
month_grid: 'mt-1 w-max border-collapse select-none',
weekdays: 'flex',
weekday: 'flex size-9 items-center justify-center text-xs/6 font-normal text-zinc-500 dark:text-zinc-400',
week: 'mt-1 flex',
day: 'relative size-9 p-0 text-center',
day_button:
'inline-flex size-9 cursor-pointer items-center justify-center rounded-md text-sm/6 font-normal text-zinc-950 hover:bg-zinc-950/5 focus:outline-hidden focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-accent-500 dark:text-white dark:hover:bg-white/10',
selected: '*:bg-accent-600! *:text-white! dark:*:bg-accent-500! dark:*:text-white!',
today: '*:font-semibold!',
outside: '*:text-zinc-950/30! dark:*:text-white/30!',
disabled: 'pointer-events-none opacity-40',
hidden: 'invisible',
}
const calendarPanel =
'z-50 rounded-xl border border-zinc-950/10 bg-white p-3 shadow-lg outline-none dark:border-white/10 dark:bg-zinc-800'
const pickerTrigger =
'relative flex w-full cursor-pointer items-center gap-2 rounded-lg border border-zinc-950/10 bg-transparent px-[calc(--spacing(3.5)-1px)] py-[calc(--spacing(2.5)-1px)] text-left text-base/6 text-zinc-950 hover:border-zinc-950/20 focus:outline-hidden sm:px-[calc(--spacing(3)-1px)] sm:py-[calc(--spacing(1.5)-1px)] sm:text-sm/6 dark:border-white/10 dark:bg-white/5 dark:text-white dark:hover:border-white/20'
const pickerControl = [
'relative block w-full',
'before:absolute before:inset-px before:rounded-[calc(var(--radius-lg)-1px)]',
'before:bg-white before:shadow-sm dark:before:hidden',
'after:pointer-events-none after:absolute after:inset-0 after:rounded-lg after:ring-transparent after:ring-inset sm:focus-within:after:ring-2 sm:focus-within:after:ring-accent-500',
]
export type DateRangeValue = { from: string; to: string } | null
export type DateRangePreset = { label: string; range: DateRangeValue }
function shiftDays(date: Date, days: number) {
let next = new Date(date)
next.setDate(next.getDate() + days)
return next
}
export function defaultRangePresets(today = new Date()): DateRangePreset[] {
let range = (from: Date, to: Date) => ({ from: toISO(from), to: toISO(to) })
return [
{ label: 'Last 7 days', range: range(shiftDays(today, -6), today) },
{ label: 'Last 30 days', range: range(shiftDays(today, -29), today) },
{ label: 'This month', range: range(new Date(today.getFullYear(), today.getMonth(), 1), today) },
{
label: 'Last month',
range: range(
new Date(today.getFullYear(), today.getMonth() - 1, 1),
new Date(today.getFullYear(), today.getMonth(), 0)
),
},
{ label: 'This year', range: range(new Date(today.getFullYear(), 0, 1), today) },
{ label: 'All time', range: null },
]
}
export function DateRangePicker({
value,
onChange,
placeholder = 'Select date range',
locale = 'en-US',
months = 2,
presets,
className,
...props
}: {
value: DateRangeValue
onChange: (value: DateRangeValue) => void
placeholder?: string
locale?: string
months?: 1 | 2
presets?: DateRangePreset[]
} & Omit<React.ComponentPropsWithoutRef<'div'>, 'onChange' | 'children'>) {
let valueId = useId()
let from = value ? fromISO(value.from) : undefined
let to = value ? fromISO(value.to) : undefined
let label =
from && to
? `${new Intl.DateTimeFormat(locale, { month: 'short', day: 'numeric', year: from.getFullYear() === to.getFullYear() ? undefined : 'numeric' }).format(from)} – ${new Intl.DateTimeFormat(locale, { month: 'short', day: 'numeric', year: 'numeric' }).format(to)}`
: null
return (
<div data-slot="control" {...props} className={clsx([className, ...pickerControl])}>
<Popover>
<Headless.Input as={PickerButton} valueId={valueId} className={pickerTrigger}>
<CalendarIcon className="size-4 shrink-0 text-zinc-500 dark:text-zinc-400" />
<span id={valueId} className={clsx('min-w-0 flex-1 truncate', !label && 'text-zinc-500')}>
{label ?? placeholder}
</span>
<ChevronDownIcon className="size-4 shrink-0 text-zinc-500 dark:text-zinc-400" />
</Headless.Input>
<PopoverPanel
anchor={{ to: 'bottom start', gap: 8, padding: 8 }}
className={clsx(calendarPanel, 'flex max-w-[calc(100vw-1rem)] flex-col gap-3 sm:flex-row sm:gap-4')}
>
{({ close }) => (
<RangePanel
value={value}
onChange={onChange}
locale={locale}
months={months}
presets={presets}
close={close}
/>
)}
</PopoverPanel>
</Popover>
</div>
)
}
function RangePanel({
value,
onChange,
locale,
months,
presets,
close,
}: {
value: DateRangeValue
onChange: (value: DateRangeValue) => void
locale: string
months: 1 | 2
presets?: DateRangePreset[]
close: () => void
}) {
let { weekStartsOn, formatters } = useCalendarLocale(locale)
let [pending, setPending] = useState<DateRange | undefined>(() => {
let from = value ? fromISO(value.from) : undefined
let to = value ? fromISO(value.to) : undefined
return from && to ? { from, to } : undefined
})
let [awaitingEnd, setAwaitingEnd] = useState(false)
let [hovered, setHovered] = useState<Date | undefined>(undefined)
let start = pending?.from
let preview =
awaitingEnd && start && hovered && hovered.getTime() !== start.getTime()
? hovered < start
? { from: hovered, to: start }
: { from: start, to: hovered }
: undefined
function commit(range: DateRangeValue) {
onChange(range)
close()
}
function handleSelect(_range: DateRange | undefined, day: Date) {
if (!awaitingEnd) {
setPending({ from: day, to: undefined })
setAwaitingEnd(true)
return
}
let start = pending?.from ?? day
let [from, to] = day < start ? [day, start] : [start, day]
setPending({ from, to })
setAwaitingEnd(false)
commit({ from: toISO(from), to: toISO(to) })
}
return (
<>
{presets && presets.length > 0 && (
<div className="flex min-w-0 gap-1 overflow-x-auto sm:w-36 sm:flex-col sm:overflow-x-visible sm:border-r sm:border-zinc-950/5 sm:pr-3 dark:sm:border-white/5">
{presets.map((preset) => (
<button
key={preset.label}
type="button"
onClick={() => commit(preset.range)}
className="shrink-0 cursor-pointer rounded-md px-3 py-1.5 text-left text-base/6 whitespace-nowrap text-zinc-500 hover:bg-zinc-950/5 hover:text-zinc-950 focus:outline-hidden focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-accent-500 max-sm:bg-zinc-950/5 max-sm:text-zinc-950 sm:text-sm/6 dark:text-zinc-400 dark:hover:bg-white/5 dark:hover:text-white dark:max-sm:bg-white/5 dark:max-sm:text-white"
>
{preset.label}
</button>
))}
</div>
)}
<DayPicker
mode="range"
selected={pending}
defaultMonth={pending?.from}
onSelect={handleSelect}
numberOfMonths={months}
weekStartsOn={weekStartsOn}
formatters={formatters}
showOutsideDays
fixedWeeks
onDayMouseEnter={(day) => setHovered(day)}
onDayMouseLeave={() => setHovered(undefined)}
modifiers={{
pending_start: awaitingEnd && start ? start : [],
preview: preview ?? [],
preview_start: preview?.from ?? [],
preview_end: preview?.to ?? [],
}}
modifiersClassNames={{
pending_start: 'rounded-md *:bg-accent-600! *:text-white! dark:*:bg-accent-500! dark:*:text-white!',
preview: 'bg-accent-500/10 dark:bg-accent-500/15',
preview_start: 'rounded-l-md',
preview_end: 'rounded-r-md',
}}
classNames={{
...calendarClassNames,
months: 'relative mx-auto flex flex-col gap-4 md:flex-row',
month: 'flex flex-col [&:not(:first-child)]:max-md:hidden',
selected: '',
range_middle: 'bg-accent-500/10 dark:bg-accent-500/15',
range_start:
'rounded-l-md bg-accent-500/10 *:bg-accent-600! *:text-white! dark:bg-accent-500/15 dark:*:bg-accent-500! dark:*:text-white!',
range_end:
'rounded-r-md bg-accent-500/10 *:bg-accent-600! *:text-white! dark:bg-accent-500/15 dark:*:bg-accent-500! dark:*:text-white!',
}}
components={calendarComponents}
/>
</>
)
}Also copy Date picker.
<script lang="ts">
export type DateRangeValue = { from: string; to: string } | null
export type DateRangePreset = { label: string; range: DateRangeValue }
function isoOf(date: Date): string {
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
}
function shiftDays(date: Date, days: number): Date {
const next = new Date(date)
next.setDate(next.getDate() + days)
return next
}
export function defaultRangePresets(today: Date = new Date()): DateRangePreset[] {
const range = (from: Date, to: Date) => ({ from: isoOf(from), to: isoOf(to) })
return [
{ label: 'Last 7 days', range: range(shiftDays(today, -6), today) },
{ label: 'Last 30 days', range: range(shiftDays(today, -29), today) },
{ label: 'This month', range: range(new Date(today.getFullYear(), today.getMonth(), 1), today) },
{
label: 'Last month',
range: range(
new Date(today.getFullYear(), today.getMonth() - 1, 1),
new Date(today.getFullYear(), today.getMonth(), 0),
),
},
{ label: 'This year', range: range(new Date(today.getFullYear(), 0, 1), today) },
{ label: 'All time', range: null },
]
}
</script>
<script setup lang="ts">
import { CalendarIcon, ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon } from '@heroicons/vue/16/solid'
import { DateFormatter, getLocalTimeZone, parseDate, type DateValue } from '@internationalized/date'
import {
PopoverContent,
PopoverPortal,
PopoverRoot,
PopoverTrigger,
RangeCalendarCell,
RangeCalendarCellTrigger,
RangeCalendarGrid,
RangeCalendarGridBody,
RangeCalendarGridHead,
RangeCalendarGridRow,
RangeCalendarHeadCell,
RangeCalendarNext,
RangeCalendarPrev,
RangeCalendarRoot,
} from 'reka-ui'
import { computed, ref, shallowRef, useAttrs, useId, watch } from 'vue'
import { useField } from './fields'
defineOptions({ inheritAttrs: false })
const props = withDefaults(
defineProps<{
placeholder?: string
locale?: string
months?: 1 | 2
presets?: DateRangePreset[]
}>(),
{ placeholder: 'Select date range', locale: 'en-US', months: 2 },
)
const model = defineModel<DateRangeValue>({ default: null })
const attrs = useAttrs()
const field = useField()
const valueId = useId()
const rootAttrs = computed(() => {
const { class: _class, ...rest } = attrs
return rest
})
function parseISO(value: string) {
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) return undefined
try {
return parseDate(value)
} catch {
return undefined
}
}
type PickerRange = { start: DateValue | undefined; end: DateValue | undefined }
const open = ref(false)
const pending = shallowRef<PickerRange>({ start: undefined, end: undefined })
let hydrating = false
watch(open, (value) => {
if (!value) return
hydrating = true
const start = model.value ? parseISO(model.value.from) : undefined
const end = model.value ? parseISO(model.value.to) : undefined
pending.value = start && end ? { start, end } : { start: undefined, end: undefined }
})
watch(pending, (value) => {
if (hydrating) {
hydrating = false
return
}
if (value.start && value.end) {
applyRange({ from: value.start.toString(), to: value.end.toString() })
}
})
const timeZone = getLocalTimeZone()
const dayFormatter = computed(() => new DateFormatter(props.locale, { month: 'short', day: 'numeric' }))
const dayYearFormatter = computed(
() => new DateFormatter(props.locale, { month: 'short', day: 'numeric', year: 'numeric' }),
)
const monthFormatter = computed(() => new DateFormatter(props.locale, { month: 'long', year: 'numeric' }))
const label = computed(() => {
const from = model.value ? parseISO(model.value.from) : undefined
const to = model.value ? parseISO(model.value.to) : undefined
if (!from || !to) return null
const fromFormatter = from.year === to.year ? dayFormatter.value : dayYearFormatter.value
return `${fromFormatter.format(from.toDate(timeZone))} – ${dayYearFormatter.value.format(to.toDate(timeZone))}`
})
function applyRange(range: DateRangeValue) {
model.value = range
open.value = false
}
</script>
<template>
<span
v-bind="rootAttrs"
data-slot="control"
:class="[
attrs.class,
'relative block w-full',
'before:absolute before:inset-px before:rounded-[calc(var(--radius-lg)-1px)]',
'before:bg-white before:shadow-sm dark:before:hidden',
'after:pointer-events-none after:absolute after:inset-0 after:rounded-lg after:ring-transparent after:ring-inset sm:focus-within:after:ring-2 sm:focus-within:after:ring-accent-500',
]"
>
<PopoverRoot v-model:open="open">
<PopoverTrigger
:id="field?.controlId"
type="button"
:aria-labelledby="[field?.labelledBy.value, valueId].filter(Boolean).join(' ')"
:aria-describedby="field?.describedBy.value"
class="relative flex w-full cursor-pointer items-center gap-2 rounded-lg border border-zinc-950/10 bg-transparent px-[calc(--spacing(3.5)-1px)] py-[calc(--spacing(2.5)-1px)] text-left text-base/6 text-zinc-950 hover:border-zinc-950/20 focus:outline-hidden sm:px-[calc(--spacing(3)-1px)] sm:py-[calc(--spacing(1.5)-1px)] sm:text-sm/6 dark:border-white/10 dark:bg-white/5 dark:text-white dark:hover:border-white/20"
>
<CalendarIcon class="size-4 shrink-0 text-zinc-500 dark:text-zinc-400" />
<span :id="valueId" class="min-w-0 flex-1 truncate" :class="label ? '' : 'text-zinc-500'">
{{ label ?? props.placeholder }}
</span>
<ChevronDownIcon class="size-4 shrink-0 text-zinc-500 dark:text-zinc-400" />
</PopoverTrigger>
<PopoverPortal>
<PopoverContent
align="start"
:side-offset="8"
:collision-padding="8"
class="z-50 flex max-w-[calc(100vw-1rem)] flex-col gap-3 rounded-xl border border-zinc-950/10 bg-white p-3 shadow-lg outline-none sm:flex-row sm:gap-4 dark:border-white/10 dark:bg-zinc-800"
>
<div
v-if="presets?.length"
class="flex min-w-0 gap-1 overflow-x-auto sm:w-36 sm:flex-col sm:overflow-x-visible sm:border-r sm:border-zinc-950/5 sm:pr-3 dark:sm:border-white/5"
>
<button
v-for="preset in presets"
:key="preset.label"
type="button"
class="shrink-0 cursor-pointer rounded-md px-3 py-1.5 text-left text-base/6 whitespace-nowrap text-zinc-500 hover:bg-zinc-950/5 hover:text-zinc-950 focus:outline-hidden focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-accent-500 max-sm:bg-zinc-950/5 max-sm:text-zinc-950 sm:text-sm/6 dark:text-zinc-400 dark:hover:bg-white/5 dark:hover:text-white dark:max-sm:bg-white/5 dark:max-sm:text-white"
@click="applyRange(preset.range)"
>
{{ preset.label }}
</button>
</div>
<RangeCalendarRoot
v-slot="{ grid, weekDays }"
v-model="pending"
:number-of-months="months"
fixed-weeks
prevent-deselect
:locale="locale"
weekday-format="short"
class="relative mx-auto"
>
<div class="absolute inset-x-0 top-0 z-10 flex h-9 items-center justify-between px-1">
<RangeCalendarPrev
class="inline-flex size-7 cursor-pointer items-center justify-center rounded-md text-zinc-500 hover:bg-zinc-950/5 hover:text-zinc-950 focus:outline-hidden focus-visible:outline-2 focus-visible:outline-accent-500 disabled:opacity-30 dark:text-zinc-400 dark:hover:bg-white/5 dark:hover:text-white"
>
<ChevronLeftIcon class="size-4 shrink-0" />
</RangeCalendarPrev>
<RangeCalendarNext
class="inline-flex size-7 cursor-pointer items-center justify-center rounded-md text-zinc-500 hover:bg-zinc-950/5 hover:text-zinc-950 focus:outline-hidden focus-visible:outline-2 focus-visible:outline-accent-500 disabled:opacity-30 dark:text-zinc-400 dark:hover:bg-white/5 dark:hover:text-white"
>
<ChevronRightIcon class="size-4 shrink-0" />
</RangeCalendarNext>
</div>
<div class="flex flex-col gap-4 md:flex-row">
<div
v-for="(month, index) in grid"
:key="month.value.toString()"
:class="index > 0 ? 'max-md:hidden' : ''"
>
<p class="flex h-9 items-center justify-center text-sm/6 text-zinc-950 dark:text-white font-medium">
{{ monthFormatter.format(month.value.toDate(timeZone)) }}
</p>
<RangeCalendarGrid class="mt-1 border-collapse select-none">
<RangeCalendarGridHead>
<RangeCalendarGridRow class="flex">
<RangeCalendarHeadCell
v-for="day in weekDays"
:key="day"
class="flex size-9 items-center justify-center text-xs/6 font-normal text-zinc-500 dark:text-zinc-400"
>
{{ day }}
</RangeCalendarHeadCell>
</RangeCalendarGridRow>
</RangeCalendarGridHead>
<RangeCalendarGridBody>
<RangeCalendarGridRow
v-for="(week, weekIndex) in month.rows"
:key="`week-${weekIndex}`"
class="mt-1 flex"
>
<RangeCalendarCell
v-for="day in week"
:key="day.toString()"
:date="day"
class="relative size-9 p-0 text-center"
>
<RangeCalendarCellTrigger
:day="day"
:month="month.value"
class="inline-flex size-9 cursor-pointer items-center justify-center rounded-md text-sm/6 font-normal text-zinc-950 hover:bg-zinc-950/5 focus:outline-hidden focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-accent-500 data-disabled:pointer-events-none data-disabled:opacity-40 data-highlighted:rounded-none data-highlighted:bg-accent-500/10 data-outside-view:text-zinc-950/30 data-selected:rounded-none data-selected:bg-accent-500/10 data-selection-end:rounded-md! data-selection-end:bg-accent-600! data-selection-end:text-white! data-selection-start:rounded-md! data-selection-start:bg-accent-600! data-selection-start:text-white! data-today:font-semibold dark:text-white dark:hover:bg-white/10 dark:data-highlighted:bg-accent-500/15 dark:data-outside-view:text-white/30 dark:data-selected:bg-accent-500/15 dark:data-selection-end:bg-accent-500! dark:data-selection-end:text-white! dark:data-selection-start:bg-accent-500! dark:data-selection-start:text-white!"
/>
</RangeCalendarCell>
</RangeCalendarGridRow>
</RangeCalendarGridBody>
</RangeCalendarGrid>
</div>
</div>
</RangeCalendarRoot>
</PopoverContent>
</PopoverPortal>
</PopoverRoot>
</span>
</template>Component API
| Prop | Default | Description |
|---|---|---|
DateRangePickerrenders a <div> element | ||
value | - | The period as `{ from, to }` in ISO (`yyyy-mm-dd`), or `null` for no period. |
onChange | - | Called once both ends are picked, or when a shortcut is chosen. |
months | 2 | Calendars side by side, `1` or `2`. Narrow screens get one. |
presets | - | Shortcuts listed beside the calendar. None by default. |
placeholder | 'Select date range' | Shown while no period is picked. |
locale | 'en-US' | BCP 47 tag driving month names, weekday order and the label format. |
| Prop | Default | Description |
|---|---|---|
DateRangePickerbuilt on the Reka UI <PopoverRoot> component | ||
v-model | - | The period as `{ from, to }` in ISO (`yyyy-mm-dd`), or `null` for no period. |
months | 2 | Calendars side by side, `1` or `2`. Narrow screens get one. |
presets | - | Shortcuts listed beside the calendar. None by default. |
placeholder | 'Select date range' | Shown while no period is picked. |
locale | 'en-US' | Drives the month names, the weekday order and the label format. |
Examples
Basic example
Two calendars, no shortcuts. Click the first day, then the last: the range commits and the popover closes on the second click, so nothing needs confirming.
Value: null
'use client'
import { DateRangePicker, type DateRangeValue } from '@/components/date-range-picker'
import { useState } from 'react'
export default function Example() {
let [range, setRange] = useState<DateRangeValue>(null)
return <DateRangePicker value={range} onChange={setRange} />
}<script setup lang="ts">
import DateRangePicker, { type DateRangeValue } from '@/components/date-range-picker.vue'
import { ref } from 'vue'
const range = ref<DateRangeValue>(null)
</script>
<template>
<DateRangePicker v-model="range" />
</template>A single calendar
Pass months={1} where the popover has to stay narrow. Navigation still moves month by month with the arrows:
<DateRangePicker value={range} onChange={setRange} months={1} /><template>
<DateRangePicker v-model="range" :months="1" />
</template>With shortcuts
Pass presets to list common periods beside the calendar. defaultRangePresets() gives you the usual six (last 7 days, last 30 days, this month, last month, this year and all time), and a shortcut applies straight away:
import { DateRangePicker, defaultRangePresets } from '@/components/date-range-picker'
export default function Example() {
let [range, setRange] = useState<DateRangeValue>(null)
return <DateRangePicker value={range} onChange={setRange} presets={defaultRangePresets()} />
}<script setup lang="ts">
import DateRangePicker, { defaultRangePresets, type DateRangeValue } from '@/components/date-range-picker.vue'
import { ref } from 'vue'
const range = ref<DateRangeValue>(null)
</script>
<template>
<DateRangePicker v-model="range" :presets="defaultRangePresets()" />
</template>Presets are plain data, so pass your own array when your product reports on different periods:{ label: string, range: { from, to } | null }.
Custom shortcuts
A null range clears the period, which is what an “All time” row does:
Same picker as above, with three shortcuts of your own instead of the defaults.
<DateRangePicker
value={range}
onChange={setRange}
presets={[
{ label: 'This week', range: { from: '2026-08-17', to: '2026-08-23' } },
{ label: 'This quarter', range: { from: '2026-07-01', to: '2026-09-30' } },
{ label: 'All time', range: null },
]}
/><script setup lang="ts">
const presets = [
{ label: 'This week', range: { from: '2026-08-17', to: '2026-08-23' } },
{ label: 'This quarter', range: { from: '2026-07-01', to: '2026-09-30' } },
{ label: 'All time', range: null },
]
</script>
<template>
<DateRangePicker v-model="range" :presets="presets" />
</template>Another locale
locale changes the month names, the first day of the week and the way the period is written out:
<DateRangePicker value={range} onChange={setRange} locale="fr-FR" placeholder="Choisir une période" /><template>
<DateRangePicker v-model="range" locale="fr-FR" placeholder="Choisir une période" />
</template>In a form
Same control base as Input, so a period filter lines up with the fields around it:
The export covers every payment settled in that window.
<Field>
<Label>Reporting period</Label>
<Description>The export covers every payment settled in that window.</Description>
<DateRangePicker value={range} onChange={setRange} />
</Field><template>
<Field>
<Label>Reporting period</Label>
<Description>The export covers every payment settled in that window.</Description>
<DateRangePicker v-model="range" />
</Field>
</template>On small screens
The popover never grows past the viewport, and the second calendar is dropped below md rather than squeezed. When shortcuts are shown they move above the calendar and scroll sideways, so the period is always reachable in one thumb reach.