Keyboard shortcut: Ctrl + K
Get started

Date input

A text field that can be typed into, with a VDatePicker in a panel beside it. The mask follows the language: the field order, the separator and the placeholder are all derived from the locale.

Usage

vue
<script setup lang="ts">
import { ref } from 'vue'
import { VDateInput } from 'vectis-ui'

const date = ref<string | null>(null)
</script>

<template>
  <VDateInput v-model="date" label="Start date" show-picker />
</template>

Examples

Label, hint and icon

label and hint behave as on any field. pickerIcon changes the glyph that opens the calendar, iconStart puts an icon at the start of the field, and loading shows a spinner in place of the calendar icon. pickerIconLabel, clearLabel, loadingText and iconStartLabel rename what each of them announces.

vue

Type it or pick it from the calendar

The calendar is the only way in

Loading…

An icon at the start, a spinner at the end while something loads

<script setup lang="ts">
import { ref } from 'vue'
import { VDateInput } from 'vectis-ui'
import { calendar_today as calendarToday, schedule, search } from 'vectis-ui/icons'

const start = ref<string | null>('2026-06-10')
const deadline = ref<string | null>(null)
</script>

<template>
  <div class="column">
    <VDateInput
      v-model="start"
      label="Start date"
      hint="Type it or pick it from the calendar"
      show-picker
      :picker-icon="calendarToday"
    />

    <VDateInput
      v-model="deadline"
      label="Deadline"
      hint="The calendar is the only way in"
      mode="picker"
      :picker-icon="schedule"
    />

    <!-- The start icon is rendered before whatever else fills that end of the field. -->
    <VDateInput
      v-model="start"
      label="Filter by date"
      hint="An icon at the start, a spinner at the end while something loads"
      :icon-start="search"
      loading
    />
  </div>
</template>

<style scoped>
.column {
  display: flex;
  flex-direction: column;
  gap: var(--vectis-space-5);
  max-inline-size: 26rem;
}
</style>

Sizes

size sets the field height to 32, 40 or 48 pixels, and compact takes 4px off it. The panel keeps its own measurements.

vue
<script setup lang="ts">
import { ref } from 'vue'
import { VDateInput } from 'vectis-ui'

const rows = ref(
  (['sm', 'md', 'lg'] as const).flatMap((size) => [
    { key: size, size, compact: false, label: size, date: '2026-06-10' as string | null },
    {
      key: `${size}-compact`,
      size,
      compact: true,
      label: `${size}, compact`,
      date: '2026-06-10' as string | null,
    },
  ]),
)
</script>

<template>
  <div class="column">
    <VDateInput
      v-for="row in rows"
      :key="row.key"
      v-model="row.date"
      :size="row.size"
      :compact="row.compact"
      :label="row.label"
      show-picker
    />
  </div>
</template>

<style scoped>
.column {
  display: flex;
  flex-direction: column;
  gap: var(--vectis-space-5);
  max-inline-size: 26rem;
}
</style>

Modes

mode chooses how the value is filled in: input masks the field so only digits are typed, the calendar then being opt-in through showPicker; picker makes the calendar the only way in. Typing is reserved for a single date.

vue

No icon, no panel: the field is the whole control

showPicker adds the icon and opens the panel on focus

Nothing can be typed, so the calendar is the only way in

<script setup lang="ts">
import { ref } from 'vue'
import { VDateInput } from 'vectis-ui'

const typed = ref<string | null>('2026-06-10')
const withPicker = ref<string | null>('2026-06-10')
const readOnly = ref<string | null>('2026-06-10')
</script>

<template>
  <div class="column">
    <VDateInput
      v-model="typed"
      label="Typed, the default"
      hint="No icon, no panel: the field is the whole control"
    />

    <VDateInput
      v-model="withPicker"
      show-picker
      label="Typed, with the calendar"
      hint="showPicker adds the icon and opens the panel on focus"
    />

    <VDateInput
      v-model="readOnly"
      mode="picker"
      label="Read-only"
      hint="Nothing can be typed, so the calendar is the only way in"
    />
  </div>
</template>

<style scoped>
.column {
  display: flex;
  flex-direction: column;
  gap: var(--vectis-space-5);
  max-inline-size: 26rem;
}
</style>

Range

selection set to range makes the value a start and an end, the calendar taking the first click as one and the second as the other.

vue

Pick the first day, then the last. Typing falls back to read-only here.

2026-06-19 to 2026-06-26
<script setup lang="ts">
import { ref } from 'vue'
import { VDateInput, type DatePickerRange } from 'vectis-ui'

const period = ref<DatePickerRange>({ start: '2026-06-19', end: '2026-06-26' })
</script>

<template>
  <div class="column">
    <VDateInput
      v-model="period"
      selection="range"
      label="Period"
      hint="Pick the first day, then the last. Typing falls back to read-only here."
      clearable
    />
    <output class="value" aria-label="Chosen period">
      {{ period.start ?? 'none' }} to {{ period.end ?? 'none' }}
    </output>
  </div>
</template>

<style scoped>
.column {
  display: grid;
  gap: var(--vectis-space-3);
  max-inline-size: 26rem;
}
.value {
  font-family: var(--vectis-text-family-code);
  font-size: var(--vectis-text-body-sm-size);
  color: var(--vectis-color-text-muted);
}
</style>

Multiple dates

selection set to multiple makes the value a list, a day already in it being taken back out by clicking it again.

vue

Click a day to add it, click it again to take it back

2026-06-05, 2026-06-12
<script setup lang="ts">
import { ref } from 'vue'
import { VDateInput } from 'vectis-ui'

const dates = ref<string[]>(['2026-06-05', '2026-06-12'])
</script>

<template>
  <div class="column">
    <VDateInput
      v-model="dates"
      selection="multiple"
      label="Dates"
      hint="Click a day to add it, click it again to take it back"
      clearable
    />
    <output class="value" aria-label="Chosen dates">{{ dates.join(', ') || 'none' }}</output>
  </div>
</template>

<style scoped>
.column {
  display: grid;
  gap: var(--vectis-space-3);
  max-inline-size: 26rem;
}
.value {
  font-family: var(--vectis-text-family-code);
  font-size: var(--vectis-text-body-sm-size);
  color: var(--vectis-color-text-muted);
}
</style>

Presets

The #footer slot is a strip at the foot of the panel, for actions or for the dates a reader reaches for most. It receives close, so a button can set the value and dismiss the panel at once.

vue
<script setup lang="ts">
import { ref } from 'vue'
import { VButton, VDateInput } from 'vectis-ui'

const date = ref<string | null>(null)

/* The clock is read in the handler and never at setup: on the server there is no telling
   what day it is where the reader stands, and a value picked there would not survive
   hydration. */
function inDays(offset: number, close: () => void) {
  const day = new Date()
  day.setDate(day.getDate() + offset)
  const month = String(day.getMonth() + 1).padStart(2, '0')
  date.value = `${day.getFullYear()}-${month}-${String(day.getDate()).padStart(2, '0')}`
  close()
}
</script>

<template>
  <div class="column">
    <VDateInput v-model="date" mode="picker" label="Due date" clearable>
      <template #footer="{ close }">
        <VButton variant="ghost" tone="neutral" size="sm" @click="inDays(0, close)">Today</VButton>
        <VButton variant="ghost" tone="neutral" size="sm" @click="inDays(1, close)">
          Tomorrow
        </VButton>
        <VButton variant="ghost" tone="neutral" size="sm" @click="inDays(7, close)">
          In a week
        </VButton>
      </template>
    </VDateInput>
  </div>
</template>

<style scoped>
.column {
  max-inline-size: 26rem;
}
</style>

Bounds and closed dates

min and max bound both the choice and the navigation. disabledDates closes individual days, as a list or as a function answering for one date at a time.

vue

Between 5 and 24 June 2026: the arrows stop at the bounds

Weekends are struck through: still reachable, never choosable

<script setup lang="ts">
import { ref } from 'vue'
import { VDateInput } from 'vectis-ui'

const booking = ref<string | null>('2026-06-15')
const appointment = ref<string | null>('2026-06-16')

/* A list or a predicate: this one closes every weekend without naming a single date. */
function isWeekend(iso: string) {
  const day = new Date(`${iso}T00:00:00`).getDay()
  return day === 0 || day === 6
}
</script>

<template>
  <div class="column">
    <VDateInput
      v-model="booking"
      min="2026-06-05"
      max="2026-06-24"
      label="Booking"
      hint="Between 5 and 24 June 2026: the arrows stop at the bounds"
      show-picker
    />

    <VDateInput
      v-model="appointment"
      :disabled-dates="isWeekend"
      label="Appointment"
      hint="Weekends are struck through: still reachable, never choosable"
      show-picker
    />
  </div>
</template>

<style scoped>
.column {
  display: flex;
  flex-direction: column;
  gap: var(--vectis-space-5);
  max-inline-size: 26rem;
}
</style>

Event dots

events draws up to three dots under a day. Each takes any CSS colour and a label, which is what assistive technology reads.

vue

Open the calendar: June has four marked days

<script setup lang="ts">
import { ref } from 'vue'
import { VDateInput, type DatePickerEvent } from 'vectis-ui'

const date = ref<string | null>('2026-06-10')

/* Up to three dots a day. The colour is any CSS colour, so a token keeps it in step with
   the theme; without one the dot takes the accent. */
const events: DatePickerEvent[] = [
  { date: '2026-06-10', label: 'Kick-off' },
  { date: '2026-06-18', color: 'var(--vectis-color-danger)', label: 'Deadline' },
  { date: '2026-06-18', color: 'var(--vectis-color-warning)', label: 'Review' },
  { date: '2026-06-24', color: 'var(--vectis-color-success)', label: 'Release' },
]
</script>

<template>
  <div class="column">
    <VDateInput
      v-model="date"
      :events="events"
      mode="picker"
      label="Sprint date"
      hint="Open the calendar: June has four marked days"
    />
  </div>
</template>

<style scoped>
.column {
  max-inline-size: 26rem;
}
</style>

Custom day cells

The #day slot replaces the number inside a day and receives the ISO date along with what the cell knows about itself: whether it belongs to the month on screen, whether it can be chosen, whether it is selected, today, or inside a period being drawn.

vue

The slot replaces the day number, so what it draws follows the selection

<script setup lang="ts">
import { ref } from 'vue'
import { VDateInput } from 'vectis-ui'

const night = ref<string | null>('2026-06-15')

/* Derived from the date itself and never drawn at random: the server and the browser have
   to render the same figure, or hydration finds two calendars. */
function priceFor(iso: string) {
  return 80 + ((Number(iso.slice(-2)) * 7) % 60)
}
</script>

<template>
  <div class="column">
    <VDateInput
      v-model="night"
      mode="picker"
      min="2026-06-05"
      max="2026-06-24"
      label="Night"
      hint="The slot replaces the day number, so what it draws follows the selection"
    >
      <template #day="{ day, iso, inMonth, disabled }">
        <span class="number">{{ day }}</span>
        <small v-if="inMonth && !disabled" class="price">€{{ priceFor(iso) }}</small>
      </template>
    </VDateInput>
  </div>
</template>

<style scoped>
.column {
  max-inline-size: 26rem;
}
.number {
  line-height: 1;
}
/* The price is set back from the number rather than given a colour of its own: mixing
   `currentcolor` towards transparent keeps it legible on the page AND on the accent a
   selected day is painted with, where a muted token would disappear. */
.price {
  font-size: var(--vectis-text-caption-size);
  line-height: 1;
  color: color-mix(in oklab, currentcolor 65%, transparent);
}
</style>

Clearable

clearable adds a cross that empties the value, to the left of the calendar icon rather than in its place.

vue

The cross sits to the left of the calendar icon, never in its place

Emptying the field is then the reader's own business

<script setup lang="ts">
import { ref } from 'vue'
import { VDateInput } from 'vectis-ui'

const withCross = ref<string | null>('2026-06-10')
const withoutCross = ref<string | null>('2026-06-10')
</script>

<template>
  <div class="column">
    <VDateInput
      v-model="withCross"
      clearable
      show-picker
      label="Clearable"
      hint="The cross sits to the left of the calendar icon, never in its place"
    />

    <VDateInput
      v-model="withoutCross"
      show-picker
      label="Not clearable, the default"
      hint="Emptying the field is then the reader's own business"
    />
  </div>
</template>

<style scoped>
.column {
  display: flex;
  flex-direction: column;
  gap: var(--vectis-space-5);
  max-inline-size: 26rem;
}
</style>

Adjacent days

showAdjacentDays fills the corners of the grid with the neighbouring months, greyed and inert. selectAdjacentDays makes those days choosable as well, and picking one moves the calendar to its month.

vue

The grid starts and ends on the month itself

The corners are filled with the neighbouring months, greyed and inert

Choosing one moves the calendar to its month

<script setup lang="ts">
import { ref } from 'vue'
import { VDateInput } from 'vectis-ui'

const hidden = ref<string | null>('2026-06-10')
const shown = ref<string | null>('2026-06-10')
const selectable = ref<string | null>('2026-06-10')
</script>

<template>
  <div class="column">
    <VDateInput
      v-model="hidden"
      mode="picker"
      label="Hidden, the default"
      hint="The grid starts and ends on the month itself"
    />

    <VDateInput
      v-model="shown"
      show-adjacent-days
      mode="picker"
      label="Shown"
      hint="The corners are filled with the neighbouring months, greyed and inert"
    />

    <VDateInput
      v-model="selectable"
      select-adjacent-days
      mode="picker"
      label="Shown and choosable"
      hint="Choosing one moves the calendar to its month"
    />
  </div>
</template>

<style scoped>
.column {
  display: flex;
  flex-direction: column;
  gap: var(--vectis-space-5);
  max-inline-size: 26rem;
}
</style>

States

invalid marks the field as having an error. disabled greys it out and prevents the panel from opening. readonly shows the value frozen: nothing can be typed and no calendar is rendered, but the field keeps its contrast and takes the focus.

vue

For a rule the browser cannot check by itself

Greyed through the colour tokens, and the panel can no longer be opened

No typing, no calendar, no clear cross

<script setup lang="ts">
import { ref } from 'vue'
import { VDateInput } from 'vectis-ui'

const invalid = ref<string | null>('2026-06-10')
</script>

<template>
  <div class="column">
    <VDateInput
      v-model="invalid"
      invalid
      show-picker
      label="Invalid"
      hint="For a rule the browser cannot check by itself"
    />

    <VDateInput
      model-value="2026-06-10"
      disabled
      show-picker
      label="Disabled, with a value"
      hint="Greyed through the colour tokens, and the panel can no longer be opened"
    />

    <VDateInput model-value="2026-06-10" disabled mode="picker" label="Disabled, picker only" />

    <!-- Frozen rather than out of reach: it still takes the focus and can be copied from. -->
    <VDateInput
      model-value="2026-06-10"
      readonly
      show-picker
      clearable
      label="Read-only"
      hint="No typing, no calendar, no clear cross"
    />
  </div>
</template>

<style scoped>
.column {
  display: flex;
  flex-direction: column;
  gap: var(--vectis-space-5);
  max-inline-size: 26rem;
}
</style>

Localization

locale decides the typing order, the separator, the month and day names and the first day of the week, and takes precedence over the global locale. displayFormat is a set of Intl options for writing the date out, and applies wherever nothing is typed.

vue

displayFormat is an Intl option bag, and it only applies where nothing is typed

<script setup lang="ts">
import { ref } from 'vue'
import { VDateInput } from 'vectis-ui'

/* The mask, the month names and the first day of the week all come from the tag. Each
   field keeps its own value so switching one does not disturb the others. */
const locales = ref([
  { tag: 'en-US', label: 'en-US, month first, weeks from Sunday', date: '2026-06-10' },
  { tag: 'en-GB', label: 'en-GB, day first, weeks from Monday', date: '2026-06-10' },
  { tag: 'de-DE', label: 'de-DE, dots for separators', date: '2026-06-10' },
  { tag: 'ja-JP', label: 'ja-JP, year first', date: '2026-06-10' },
])

const written = ref<string | null>('2026-06-10')
</script>

<template>
  <div class="column">
    <VDateInput
      v-for="locale in locales"
      :key="locale.tag"
      v-model="locale.date"
      :locale="locale.tag"
      :label="locale.label"
      show-picker
    />

    <VDateInput
      v-model="written"
      locale="fr-FR"
      mode="picker"
      :display-format="{ dateStyle: 'full' }"
      label="fr-FR, written out in full"
      hint="displayFormat is an Intl option bag, and it only applies where nothing is typed"
    />
  </div>
</template>

<style scoped>
.column {
  display: flex;
  flex-direction: column;
  gap: var(--vectis-space-5);
  max-inline-size: 26rem;
}
</style>

Placement

placement names the preferred opening direction of the panel, above or below the field.

vue
<script setup lang="ts">
import { ref } from 'vue'
import { VDateInput } from 'vectis-ui'

const placements = ['bottom-start', 'bottom-end', 'top-start', 'top-end'] as const

const dates = ref<Record<string, string | null>>({
  'bottom-start': '2026-06-10',
  'bottom-end': '2026-06-10',
  'top-start': '2026-06-10',
  'top-end': '2026-06-10',
})
</script>

<template>
  <div class="grid">
    <VDateInput
      v-for="placement in placements"
      :key="placement"
      v-model="dates[placement]"
      :placement="placement"
      :label="placement"
      mode="picker"
    />
  </div>
</template>

<style scoped>
.grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
  gap: var(--vectis-space-5);
  max-inline-size: 26rem;
}
</style>

API

Props

PropTypeDefault
selectionDatePickerSelection'single' | 'range' | 'multiple''single'
What is being chosen: one date, a period between two, or several separate dates.
localestringnone
A BCP 47 locale, which decides the month and day names, the first day of the week and the order the field is typed in. It takes precedence over the design system's global locale and falls back to it.
firstDayOfWeeknumbernone
Forces the day the weeks start on, from 0 for Sunday to 6 for Saturday.
minstringnone
The earliest date that can be chosen, as an ISO string.
maxstringnone
The latest date that can be chosen, as an ISO string.
disabledDatesDatePickerMatchernone
Dates that cannot be chosen, as a list or as a function.
showAdjacentDaysbooleanfalse
Fills the corners of the grid with the greyed days of the neighbouring months.
selectAdjacentDaysbooleanfalse
Lets those neighbouring days be clicked, which implies showing them.
eventsDatePickerEvent[]none
Events to mark under the days they fall on.
modeDateInputMode'picker' | 'input''input'
Whether the field can be typed into, using the numeric form of the reader's language, or is filled from the calendar alone, which is picker. Typing is reserved for choosing a single date: a period or a list falls back to picker, there being no sensible way to type either. It is a different question from readonly, which freezes the field by every route at once.
showPickerbooleanfalse
Offers the date picker alongside a field that can be typed into: an icon at the end of the field, and a panel that opens on focus. It means nothing in picker mode, where the calendar is already the only way to choose.
labelstringnone
The label above the field.
hintstringnone
A line of help under the field.
placeholderstringnone
What the field says while empty.
sizeDateInputSize'sm' | 'md' | 'lg''md'
The height of the field: 32, 40 or 48 pixels.
compactbooleanfalse
Takes 4px off the height.
disabledbooleanfalse
Makes the field unusable, greyed out through the colour tokens.
readonlybooleanfalse
Shows the date without letting it be changed: nothing can be typed, there is no calendar and no clear cross, and the attributes announcing a panel go with it. The field keeps the focus and can be copied from, which is what separates it from disabled.
invalidbooleanfalse
Marks the field as invalid, for a rule of your own.
iconStartIconSourcenone
An icon inside the field, at the start. Decorative until a @click:icon-start listener turns it into a button.
iconStartLabelstringnone
What the start icon does, in words, once it is clickable.
pickerIconLabelstringnone
What the end icon does, in words. It names the button that opens the calendar, and falls back to the design system dictionary.
loadingbooleanfalse
Shows a spinner in place of the calendar icon. It says that something is being loaded and changes nothing else: the field can still be typed into and the panel still opens.
loadingTextstringnone
What screen readers announce while the spinner turns. It falls back to the design system dictionary.
clearablebooleanfalse
Offers a cross that empties the value, shown before the end icon.
clearLabelstringnone
What that cross does, in words. It falls back to the design system dictionary.
pickerIconIconSourcecalendar_today
The icon that opens the date picker, at the end of the field. The clear cross appears to its left rather than in its place, and no icon is rendered at all when there is no panel to open.
displayFormatIntl.DateTimeFormatOptions{ day: 'numeric', month: 'short', year: 'numeric' }
How the date is written out in the field. It has no effect on a field being typed into, which necessarily shows the numeric form one types, so it concerns picker mode and the period and list selections.
placementDateInputPlacement'bottom' | 'bottom-start' | 'bottom-end' | 'top' | 'top-start' | 'top-end''bottom-start'
Where the panel opens relative to the field.
v-modelDatePickerValuenull
The date or dates chosen, in the shape selection calls for. While the reader types, it is only written once what they have entered is a complete and acceptable date; an unfinished or refused entry leaves it untouched and is reverted when they leave the field.

Events

EventType
click:icon-start[event: MouseEvent]
The start icon was clicked. Attaching this listener is what turns that icon into a real button, which then needs iconStartLabel.
clear[]
The clear cross emptied the field. The value has already been reset.

Slots

SlotType
start{}
Content at the start of the field, rendered after iconStart rather than in its place.
value-end{}
Controls of your own inside the field, placed before the ones the field owns: the clear cross and the icon that opens the panel. Those two are the component own affordance, which is why there is no end slot here.
dayDatePickerDaySlotProps
What a day cell shows, handed straight to the calendar.
footerDateInputFooterSlotProps
The strip at the foot of the panel: actions, or preset dates such as today. It receives close, which is what lets one of those buttons dismiss the panel.

Types

The types the tables above name, written as the library declares them. The ones carrying export can be imported from vectis-ui to type your own code; the others are the shape of what a slot hands out.

export interface BuiltinIcon {
  name: string
  paths: readonly [string] | readonly [string, string]
}
export interface DateInputFooterSlotProps {
  close: () => void
}
export interface DatePickerDaySlotProps {
  iso: string
  day: number
  inMonth: boolean
  disabled: boolean
  selected: boolean
  today: boolean
  inRange: boolean
  events: DatePickerEvent[]
}
export interface DatePickerEvent {
  date: string
  color?: string
  label?: string
}
export type DatePickerMatcher = string[] | ((iso: string) => boolean)
export interface DatePickerRange {
  start: string | null
  end: string | null
}
export type DatePickerValue = string | null | DatePickerRange | string[]
export type IconRender =
  | { path: string; viewBox?: string }
  | { component: Component; props?: Record<string, unknown> }
  | { src: string }
  | { text: string; class?: string }
  | { class: string }
export type IconSource = string | BuiltinIcon | IconRender