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>
Usage
import { DateRangePicker } from '@hina-ui/vue'
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.
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>
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.
<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>
Clearable
clearable adds a clear button after the segments that empties both sides and sets the value to null.
<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>
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.
<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>
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.
<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>
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.
<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>
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
nullwhile 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
DateRangeFieldandRangeCalendarfor 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-labeloraria-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 |