Skip to content
  1. Inputs
  2. Date range picker

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-picker
pnpm add react-day-picker
yarn add react-day-picker
bun add react-day-picker
npm install reka-ui @internationalized/date
pnpm add reka-ui @internationalized/date
yarn add reka-ui @internationalized/date
bun add reka-ui @internationalized/date

This 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

PropDefaultDescription
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.
months2Calendars 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.
PropDefaultDescription
DateRangePickerbuilt on the Reka UI <PopoverRoot> component
v-model-The period as `{ from, to }` in ISO (`yyyy-mm-dd`), or `null` for no period.
months2Calendars 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.