DateRangePicker

Types a date range or picks it from a calendar.

9
4
2026
9
12
2026

Value: 2026-09-04 to 2026-09-12

<script setup lang="ts">
  import { ref } from 'vue'
  import { DateRangePicker, Stack, Text, type DateRangeValue } from '@hina-ui/vue'

  const range = ref<DateRangeValue | null>({ start: '2026-09-04', end: '2026-09-12' })
</script>

<template>
  <Stack gap="sm" align="start">
    <DateRangePicker v-model="range" clearable aria-label="Event period" class="w-96" />
    <Text tone="muted" size="sm">
      Value: {{ range ? `${range.start ?? 'empty'} to ${range.end ?? 'empty'}` : 'empty' }}
    </Text>
  </Stack>
</template>
vue

Usage

import { DateRangePicker } from '@hina-ui/vue'
ts

The date range picker combines the segmented field of DateRangeField with RangeCalendar: type the start and end dates straight into the segments, or click the button at the end to pick them from a calendar. v-model binds a { start, end } object whose fields are YYYY-MM-DD ISO 8601 strings, the same shape both parts use. Attributes it does not declare land on the group element that wraps the segments, so name it with aria-label or aria-labelledby.

mm
dd
yyyy
mm
dd
yyyy

Value: empty

<script setup lang="ts">
  import { ref } from 'vue'
  import { DateRangePicker, Stack, Text, type DateRangeValue } from '@hina-ui/vue'

  const range = ref<DateRangeValue | null>(null)
</script>

<template>
  <Stack gap="sm" align="start">
    <DateRangePicker v-model="range" aria-label="Release window" class="w-96" />
    <Text tone="muted" size="sm">
      Value: {{ range ? `${range.start ?? 'empty'} to ${range.end ?? 'empty'}` : 'empty' }}
    </Text>
  </Stack>
</template>
vue

Examples

Range and unavailable dates

min and max constrain both the field and the calendar, unavailable marks dates as unavailable and maximumDays caps how many days a span may contain.

9
8
2026
9
11
2026
<script setup lang="ts">
  import { ref } from 'vue'
  import { DateRangePicker, type DateRangeValue } from '@hina-ui/vue'

  const range = ref<DateRangeValue | null>({ start: '2026-09-08', end: '2026-09-11' })
  const weekend = (value: string) => [0, 6].includes(new Date(`${value}T00:00`).getDay())
</script>

<template>
  <DateRangePicker
    v-model="range"
    min="2026-09-01"
    max="2026-09-30"
    :maximum-days="7"
    :unavailable="weekend"
    aria-label="Event period"
    class="w-96"
  />
</template>
vue

Clearable

clearable adds a clear button after the segments that empties both sides and sets the value to null.

9
4
2026
9
12
2026
<script setup lang="ts">
  import { ref } from 'vue'
  import { DateRangePicker, type DateRangeValue } from '@hina-ui/vue'

  const range = ref<DateRangeValue | null>({ start: '2026-09-04', end: '2026-09-12' })
</script>

<template>
  <DateRangePicker v-model="range" clearable aria-label="Event period" class="w-96" />
</template>
vue

Sizes

size has three steps, sm, md and lg, matching the input of the same step in height; the calendar in the popover uses the same step.

9
4
2026
9
12
2026
9
4
2026
9
12
2026
9
4
2026
9
12
2026
<script setup lang="ts">
  import { DateRangePicker, Stack } from '@hina-ui/vue'

  const range = { start: '2026-09-04', end: '2026-09-12' }
</script>

<template>
  <Stack class="w-96">
    <DateRangePicker size="sm" :model-value="range" aria-label="Small" />
    <DateRangePicker size="md" :model-value="range" aria-label="Medium" />
    <DateRangePicker size="lg" :model-value="range" aria-label="Large" />
  </Stack>
</template>
vue

States

invalid gives the field a warning color, disabled disables the group and readonly makes it read only. variant="secondary" is the flat form used inside a surface.

9
4
2026
9
12
2026
9
4
2026
9
12
2026
9
4
2026
9
12
2026
9
4
2026
9
12
2026
<script setup lang="ts">
  import { DateRangePicker, Stack } from '@hina-ui/vue'

  const range = { start: '2026-09-04', end: '2026-09-12' }
</script>

<template>
  <Stack class="w-96">
    <DateRangePicker :model-value="range" invalid aria-label="Invalid" />
    <DateRangePicker :model-value="range" disabled aria-label="Disabled" />
    <DateRangePicker :model-value="range" readonly aria-label="Read only" />
    <DateRangePicker :model-value="range" variant="secondary" aria-label="Secondary" />
  </Stack>
</template>
vue

In a form

Inside a FormField the label is linked to the input, and the error message is rendered by the field; validation rules and submission belong to the Form. Write the completeness check on the range object itself for the error to land on this field.

mm
dd
yyyy
mm
dd
yyyy
<script setup lang="ts">
  import { reactive, ref } from 'vue'
  import * as v from 'valibot'
  import { Button, DateRangePicker, Form, FormField, Text, type DateRangeValue } from '@hina-ui/vue'

  const schema = v.object({
    trip: v.pipe(
      v.nullable(v.object({ start: v.nullable(v.string()), end: v.nullable(v.string()) })),
      v.check(value => !!value?.start && !!value?.end, 'Pick the whole trip'),
    ),
  })

  const values = reactive({ trip: null as DateRangeValue | null })
  const saved = ref('')

  async function save(data: unknown) {
    await new Promise(resolve => setTimeout(resolve, 600))
    saved.value = JSON.stringify(data)
  }
</script>

<template>
  <Form v-slot="{ submitting }" :values="values" :rules="schema" class="w-80" @submit="save">
    <FormField name="trip" label="Trip dates" required>
      <DateRangePicker v-model="values.trip" />
    </FormField>
    <Button type="submit" :loading="submitting" class="self-start">Save</Button>
    <Text v-if="saved" tone="muted" size="sm">Saved: {{ saved }}</Text>
  </Form>
</template>
vue

Behavior

  • Clicking the button at the end opens the calendar with focus on the current start date. The first click in the calendar sets the start, updating the value to the start plus null while the calendar stays open; the second sets the end, after which the value updates, the calendar closes and focus returns to the button.
  • The calendar is a modal popover: focus stays inside it while open, the page stops scrolling, and clicking anywhere outside it, the field included, closes it.
  • Esc closes the calendar and returns focus to the button. Inside the month and year views, Esc first steps back a level.
  • The calendar opens below the field, aligned with its left edge.

Accessibility

  • See DateRangeField and RangeCalendar for the semantics of the segments and the calendar.
  • The button that opens the calendar carries a localized name and aria-expanded.
  • Name the group with aria-label or aria-labelledby.

API

Props

Prop
Type
Default
Description
modelValue
DateRangeValue | null
null
The selected span, both ends YYYY-MM-DD
open
boolean
false
Whether the calendar is open, v-model:open
placeholder
string
Today
Where the segments and calendar start while empty
min
string
—
Lower bound of the selectable range
max
string
—
Upper bound of the selectable range
maximumDays
number
—
Maximum number of days a span may contain
unavailable
(date: string) => boolean
—
Whether a given date is unavailable
weekStartsOn
0 | 1 | 2 | 3 | 4 | 5 | 6
Locale
First day of the week
weekdayFormat
'narrow' | 'short'
'narrow'
Format of the weekday names
fixedWeeks
boolean
true
Whether the calendar always shows six weeks
clearable
boolean
false
Whether the clear button is shown
readonly
boolean
false
Whether the field is read only
name
string
—
Form field name
variant
'primary' | 'secondary'
'primary'
Variant
size
'sm' | 'md' | 'lg'
'md'
Size
disabled
boolean
false
Whether the field is disabled
invalid
boolean
false
Whether validation failed
class
string
—
Classes appended to the root element

DateRangeValue is { start: string | null; end: string | null } and is exported from the package entry.

Slots

Slot
Description
leading
Content of the leading square

Events

Event
Payload
Description
update:modelValue
value: DateRangeValue | null
The value changed
update:open
open: boolean
The calendar opened or closed
clear
—
The clear was clicked