Vitral 0.2
Form

DateRange

Two dates and the days between them, picked on two months side by side or one, with the span drawn before it is committed.

Import

main.ts
import { DateRange } from '@vitral/vue';

Two months

The default. Pick a start, then an end; the band between them follows the pointer before you commit. Each calendar stops at its own month — the days either side are left blank rather than repeating the month next door.

Your stay
6 nights
<script setup lang="ts">import { DateRange, type DateRangeValue, StackPanel } from '@vitral/vue';import { computed, ref } from 'vue'; const at = (offset: number) => {    const d = new Date();    d.setDate(d.getDate() + offset);    d.setHours(0, 0, 0, 0);    return d;}; const stay = ref<DateRangeValue>({ start: at(3), end: at(9) });const nights = computed(() => (stay.value.start && stay.value.end ? Math.round((stay.value.end.getTime() - stay.value.start.getTime()) / 86400000) : 0));</script> <template>    <StackPanel spacing="0.375rem">        <span id="dr-stay">Your stay</span>        <DateRange v-model="stay" :min-date="at(0)" show-clear-button aria-labelledby="dr-stay" />        <small style="color: var(--vt-text-muted-color)">{{ nights ? `${nights} night${nights === 1 ? '' : 's'}` : 'No dates yet' }}</small>    </StackPanel></template>

One month

The same range in a single calendar, for a form with no room for two. With no neighbour to repeat, the days either side of the month come back.

Dates
Empty
<script setup lang="ts">import { DateRange, type DateRangeValue, StackPanel } from '@vitral/vue';import { ref } from 'vue'; const single = ref<DateRangeValue>({ start: null, end: null });</script> <template>    <StackPanel spacing="0.375rem">        <span id="dr-one">Dates</span>        <DateRange v-model="single" :months="1" placeholder="Pick two dates" aria-labelledby="dr-one" />        <small style="color: var(--vt-text-muted-color)">{{ single.start ? `${single.start.toLocaleDateString()} → ${single.end?.toLocaleDateString() ?? '…'}` : 'Empty' }}</small>    </StackPanel></template>

With the spans people ask for by name

The footer is a slot: presets, a count of nights, whatever the form needs.

<script setup lang="ts">import { Button, DateRange, type DateRangeValue, StackPanel } from '@vitral/vue';import { ref } from 'vue'; const at = (offset: number) => {    const d = new Date();    d.setDate(d.getDate() + offset);    d.setHours(0, 0, 0, 0);    return d;}; const report = ref<DateRangeValue>({ start: at(-30), end: at(0) }); // A range picker is usually next to a few spans people ask for by name.const presets = [    { label: 'Last 7 days', days: 7 },    { label: 'Last 30 days', days: 30 },    { label: 'Last 90 days', days: 90 }];const usePreset = (days: number) => (report.value = { start: at(-days + 1), end: at(0) });</script> <template>    <DateRange v-model="report" :max-date="at(0)" aria-label="Reporting period">        <template #footer="{ nights: n, clear }">            <StackPanel orientation="horizontal" spacing="0.375rem" align="center" wrap as="span">                <Button v-for="p in presets" :key="p.days" :label="p.label" severity="secondary" variant="text" size="small" @click="usePreset(p.days)" />            </StackPanel>            <small style="color: var(--vt-text-muted-color)">{{ n + 1 }} days · <a href="#" @click.prevent="clear">clear</a></small>        </template>    </DateRange></template>

Inline

Without the text box or the popup.

September 2026
SuMoTuWeThFrSa
12345
6789101112
13141516171819
20212223242526
27282930
October 2026
SuMoTuWeThFrSa
123
45678910
11121314151617
18192021222324
25262728293031
<script setup lang="ts">import { DateRange, type DateRangeValue } from '@vitral/vue';import { ref } from 'vue'; const at = (offset: number) => {    const d = new Date();    d.setDate(d.getDate() + offset);    d.setHours(0, 0, 0, 0);    return d;}; const inline = ref<DateRangeValue>({ start: at(1), end: at(5) });</script> <template>    <DateRange v-model="inline" inline aria-label="Dates" /></template>

Showing the neighbouring days anyway

`show-other-months` overrides what the number of calendars decided. Two months with their edges drawn: February’s last days appear again as March’s first, and a range spanning the boundary is painted twice.

September 2026
SuMoTuWeThFrSa
303112345
6789101112
13141516171819
20212223242526
27282930123
45678910
October 2026
SuMoTuWeThFrSa
27282930123
45678910
11121314151617
18192021222324
25262728293031
1234567
<script setup lang="ts">import { DateRange, type DateRangeValue } from '@vitral/vue';import { ref } from 'vue'; const at = (offset: number) => {    const d = new Date();    d.setDate(d.getDate() + offset);    d.setHours(0, 0, 0, 0);    return d;}; const inline = ref<DateRangeValue>({ start: at(1), end: at(5) });</script> <template>    <DateRange v-model="inline" inline show-other-months aria-label="Dates, edges drawn" /></template>

The shape of it

<script setup lang="ts">import { DateRange, type DateRangeValue } from '@vitral/vue';import { ref } from 'vue'; const stay = ref<DateRangeValue>({ start: null, end: null });</script> <template>    <DateRange v-model="stay" :min-date="new Date()" show-clear-button aria-label="Stay" /></template>

API

Read from packages/vue/src/components/DateRange/types.ts, so it says what the component actually accepts.

Props

NameTypeDescription
monthsnumberHow many months to show side by side. Two is the default and the reason this component exists: a span that crosses a month boundary is chosen in one gesture rather than by paging. One shows the same range in a single calendar.
showOtherMonthsbooleanWhether the days either side of each month are drawn. Left unset, they are on a single calendar and off on several: with two months side by side the last days of one are the first days of the next, so the same date appears twice, a range paints across both copies, and the boundary between the calendars stops meaning anything. The cells stay — the grid keeps its six rows and its height — they are simply left empty.
dateFormatstringA `formatDate` pattern; defaults to the locale's `dateFormat`.
separatorstringBetween the two dates in the text box.
minDateDate | null—
maxDateDate | null—
disabledDatesDate[]—
disabledDaysnumber[]Weekdays that cannot be chosen, 0 = Sunday.
showClearButtonbooleanA footer button that empties the range.
firstDayOfWeeknumber0 = Sunday; defaults to the locale's `firstDayOfWeek`.
placeholderstring—
inlinebooleanShow the calendars in place, without a text box or popup.
sizeSize—
variantInputVariantDefaults to the plugin's `inputVariant`.
invalidboolean—
disabledboolean—
readonlyboolean—
fluidboolean—
placementOverlayPlacement—
appendTostring`'body'` (the default), `'self'` to render in place, or a selector.

Plus pt, dt and unstyled from BaseProps, see pass-through and unstyled mode.

Emits

EventPayloadDescription
rangeSelectrange: DateRangeValueBoth ends are down. Fires once a range is whole, not on the first press.
clear——
show——
hide——
monthChangeevent: { month: number; year: number }—

Slots

NameSlot propsDescription
date(props: { date: Date; day: number; today: boolean; inRange: boolean; end: 'start' | 'end' | null; disabled: boolean; otherMonth: boolean })A day's content.
footer(props: { range: DateRangeValue; nights: number; clear: () => void })Under the calendars: presets, a count of nights, a pair of buttons.