Keyboard shortcut: Ctrl + K
Get started

Date picker

An inline calendar grid. Every date it holds is a plain local-time YYYY-MM-DD string and never a Date, so a value cannot shift a day across time zones.

Usage

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

const date = ref('2026-06-10')
</script>

<template>
  <VDatePicker v-model="date" />
</template>

Examples

Range

selection set to range makes the value a start and an end, the span under the pointer being previewed between the two clicks.

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

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

<template>
  <div class="column">
    <VDatePicker v-model="period" selection="range" />
    <output class="value" aria-label="Chosen period">
      {{ period.start ?? 'none' }} to {{ period.end ?? 'none' }}
    </output>
  </div>
</template>

<style scoped>
.column {
  display: grid;
  justify-items: start;
  gap: var(--vectis-space-3);
}
.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 coming back out when it is clicked again.

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

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

<template>
  <div class="column">
    <VDatePicker v-model="dates" selection="multiple" />
    <output class="value" aria-label="Chosen dates">{{ dates.join(', ') || 'none' }}</output>
  </div>
</template>

<style scoped>
.column {
  display: grid;
  justify-items: start;
  gap: var(--vectis-space-3);
}
.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 under the grid, for actions or for the dates a reader reaches for most. The buttons in it write the model like any other control.

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

const date = ref('2026-06-10')

/* The clock is read in the handler and never at setup: the server cannot know what day it
   is where the reader stands, and a value taken there would not survive hydration. */
function inDays(offset: number) {
  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')}`
}
</script>

<template>
  <VDatePicker v-model="date">
    <template #footer>
      <VButton variant="ghost" tone="neutral" size="sm" @click="inDays(0)">Today</VButton>
      <VButton variant="ghost" tone="neutral" size="sm" @click="inDays(1)">Tomorrow</VButton>
      <VButton variant="ghost" tone="neutral" size="sm" @click="inDays(7)">In a week</VButton>
    </template>
  </VDatePicker>
</template>

Disabled dates

disabledDates takes a list of days, or a function asked about one date at a time. A closed day stays visible, struck through, and reachable by keyboard.

vue
a predicate
MonTueWedThuFriSatSun
a list
MonTueWedThuFriSatSun
<script setup lang="ts">
import { ref } from 'vue'
import { VDatePicker, VTypography } from 'vectis-ui'

const appointment = ref('2026-06-16')
const holiday = ref('2026-06-16')

/* A predicate answers for one date at a time, which is what makes a rule such as "no
   weekends" one line rather than an enumeration. */
function isWeekend(iso: string) {
  const day = new Date(`${iso}T00:00:00`).getDay()
  return day === 0 || day === 6
}

/* The other form: a plain list, for days that follow no rule. */
const closedDays = ['2026-06-11', '2026-06-12', '2026-06-25']
</script>

<template>
  <div class="row">
    <div class="group">
      <VTypography variant="overline" tone="muted">a predicate</VTypography>
      <VDatePicker v-model="appointment" :disabled-dates="isWeekend" />
    </div>

    <div class="group">
      <VTypography variant="overline" tone="muted">a list</VTypography>
      <VDatePicker v-model="holiday" :disabled-dates="closedDays" />
    </div>
  </div>
</template>

<style scoped>
.row {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: var(--vectis-space-6);
}
.group {
  display: grid;
  gap: var(--vectis-space-2);
}
</style>

Minimum and maximum

min and max bound the navigation as well as the choice, in every view.

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

const date = ref('2026-06-15')
</script>

<template>
  <VDatePicker v-model="date" min="2026-06-05" max="2026-06-24" />
</template>

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
MonTueWedThuFriSatSun
<script setup lang="ts">
import { ref } from 'vue'
import { VDatePicker, type DatePickerEvent } from 'vectis-ui'

const date = ref('2026-06-10')

/* Up to three dots a day, in any CSS colour. A token keeps them in step with both themes,
   and a dot given none 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>
  <VDatePicker v-model="date" :events="events" />
</template>

Adjacent days

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

vue
hidden, the default
MonTueWedThuFriSatSun
shown
MonTueWedThuFriSatSun
1
2
3
4
5
6
7
8
9
10
11
12
shown and choosable
MonTueWedThuFriSatSun
<script setup lang="ts">
import { ref } from 'vue'
import { VDatePicker, VTypography } from 'vectis-ui'

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

<template>
  <div class="row">
    <div class="group">
      <VTypography variant="overline" tone="muted">hidden, the default</VTypography>
      <VDatePicker v-model="hidden" />
    </div>

    <div class="group">
      <VTypography variant="overline" tone="muted">shown</VTypography>
      <VDatePicker v-model="shown" show-adjacent-days />
    </div>

    <div class="group">
      <VTypography variant="overline" tone="muted">shown and choosable</VTypography>
      <VDatePicker v-model="selectable" select-adjacent-days />
    </div>
  </div>
</template>

<style scoped>
.row {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: var(--vectis-space-6);
}
.group {
  display: grid;
  gap: var(--vectis-space-2);
}
</style>

Localization

locale decides the month and day names and the day the weeks start on, and takes precedence over the global locale. firstDayOfWeek overrides the day that locale would have chosen.

vue
en-US, weeks from Sunday
SunMonTueWedThuFriSat
fr-FR, weeks from Monday
lun.mar.mer.jeu.ven.sam.dim.
ja-JP
en-US, weeks forced to Monday
MonTueWedThuFriSatSun
<script setup lang="ts">
import { ref } from 'vue'
import { VDatePicker, VTypography } from 'vectis-ui'

/* Each keeps its own value, so switching one does not disturb the others. */
const locales = ref([
  { tag: 'en-US', caption: 'en-US, weeks from Sunday', date: '2026-06-10' },
  { tag: 'fr-FR', caption: 'fr-FR, weeks from Monday', date: '2026-06-10' },
  { tag: 'ja-JP', caption: 'ja-JP', date: '2026-06-10' },
])

const forced = ref('2026-06-10')
</script>

<template>
  <div class="row">
    <div v-for="locale in locales" :key="locale.tag" class="group">
      <VTypography variant="overline" tone="muted">{{ locale.caption }}</VTypography>
      <VDatePicker v-model="locale.date" :locale="locale.tag" />
    </div>

    <div class="group">
      <VTypography variant="overline" tone="muted">en-US, weeks forced to Monday</VTypography>
      <VDatePicker v-model="forced" locale="en-US" :first-day-of-week="1" />
    </div>
  </div>
</template>

<style scoped>
.row {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: var(--vectis-space-6);
}
.group {
  display: grid;
  gap: var(--vectis-space-2);
}
</style>

API

Props

PropTypeDefault
selectionDatePickerSelection'single' | 'range' | 'multiple''single'
What the reader is picking: a single date, a period between two dates, or any number of separate dates. It determines the shape of the value.
localestringnone
A BCP 47 locale, which decides the month and day names and the first day of the week. It takes precedence over the design system's global locale and falls back to it, which is why it has no literal default.
firstDayOfWeeknumbernone
Forces the day the weeks start on, 0 for Sunday through 6 for Saturday. Left out, the locale decides.
minstringnone
The earliest selectable date, as an ISO string. Neither navigation nor selection goes back beyond it.
maxstringnone
The latest selectable date, as an ISO string. Neither navigation nor selection goes past it.
disabledDatesDatePickerMatchernone
Dates that cannot be chosen, given as a list of ISO strings or as a function. They stay visible, struck through, and can still be reached with the keyboard.
showAdjacentDaysbooleanfalse
Also fills the empty corners of the grid with the greyed days of the neighbouring months.
selectAdjacentDaysbooleanfalse
Lets those neighbouring days be clicked, which moves the calendar to their month. A clickable day has to be visible, so this implies showing them.
eventsDatePickerEvent[]none
The events to mark, as up to three coloured dots under the day they fall on.
disabledbooleanfalse
Makes the whole calendar unusable: no date can be chosen, no month reached, and everything greys out through the colour tokens.
readonlybooleanfalse
Shows what is selected without letting it be changed. The calendar can still be read and walked through, another month or another year, which is what separates it from disabled.
labelstringnone
The accessible name of the whole picker, its header and its grid together. A range shown as two calendars side by side needs one each, or a screen reader announces the same group twice. It falls back to the dictionary, and a consumer aria-label wins.
v-modelDatePickerValuenull
What is selected, and its shape follows selection: an ISO string for a single date, a start and end pair for a period, an array for several. Nothing is selected to begin with.

Events

EventType
select[value: DatePickerValue]
A date was chosen, with the value as it now stands. Chosen is not finished: a period or a list is still being built after it, which is why VTimePicker names its own end of a choice confirm.

Slots

SlotType
dayDatePickerDaySlotProps
Replaces the content of a day cell, to show a price or an availability under the number. It receives everything known about that day, including whether it belongs to the displayed month.
footer{}
The strip under the grid, for actions such as Close or Save, or for preset dates.

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 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[]

CSS variables

TokenValue
--vectis-control-size-date-picker-cell2.5rem
--vectis-control-size-date-picker-dayvar(--vectis-control-height-md)
--vectis-control-size-date-picker-dot0.25rem
--vectis-control-size-date-picker-nav-min5.375rem