Skip to content

Calendars

Calendars are used to allow users to select a date, for example to select a date of birth or a date for a reservation.

Calendar components in Formwerk can be used with a date field or as a standalone component.

  • Labeling, Descriptions, and error messages are automatically linked to the calendar elements.
  • Supports different views (week, month, year) to allow for large date range navigation.
  • Support for multiple calendars (e.g: islamic-umalqura, japanese, etc).
  • Supports minimum and maximum date boundaries.
  • Validation support with native HTML constraints or Standard Schemas.
  • Auto directionality based on the locale.
  • Cell states for today, selected, disabled and outside month dates.
  • Support for v-model binding.
  • Comprehensive keyboard shortcuts support.
KeyDescription
ArrowDown
Moves the focus to the same day of the next week.
ArrowUp
Moves the focus to the same day of the previous week.
ArrowLeft
Moves the focus to the previous day.
ArrowRight
Moves the focus to the next day.
Home
Moves the focus to the first day of the current month. If already on the first day, moves the focus to the first day of the previous month.
End
Moves the focus to the last day of the current month. If already on the last day, moves the focus to the last day of the next month.
PageUp
Moves the focus to the same day of the previous month.
PageDown
Moves the focus to the same day of the next month.
KeyDescription
ArrowDown
Moves the focus to the next quarter.
ArrowUp
Moves the focus to the previous quarter.
ArrowLeft
Moves the focus to the previous month.
ArrowRight
Moves the focus to the next month.
Home
Moves the focus to the first month of the year. If already on the first month, moves the focus to the first month of the previous year.
End
Moves the focus to the last month of the year. If already on the last month, moves the focus to the last month of the next year.
PageUp
Moves the focus to the same month of the previous year.
PageDown
Moves the focus to the same month of the next year.
KeyDescription
ArrowDown
Moves the focus to the next year row (+3 years).
ArrowUp
Moves the focus to the previous year row (-3 years).
ArrowLeft
Moves the focus to the previous year.
ArrowRight
Moves the focus to the next year.
Previous Month
September 2025
Grid Label
Next Month
Sun
Mon
Tue
Wed
Thu
Fri
Sat
31
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Calendar Cell
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
1
2
3
4
5
6
7
8
9
10
11
Grid (7 columns)

You can start by importing the useCalendar composable and using it in your calendar component.

The useCalendar composable returns binding objects for the elements shown in the anatomy. You will use v-bind to bind them to the corresponding DOM elements.

<script setup lang="ts">
import Calendar from './Calendar.vue';
import { ref } from 'vue';
const date = ref(new Date('2025-09-14'));
</script>
<template>
<Calendar label="Select a date" v-model="date" />
</template>
<script setup lang="ts">
import { useCalendar, CalendarCell, type CalendarProps } from '@formwerk/core';
const props = defineProps<CalendarProps>();
const {
calendarProps,
gridProps,
nextButtonProps,
previousButtonProps,
gridLabelProps,
gridLabel,
currentView,
errorMessage,
errorMessageProps,
} = useCalendar(props);
</script>
<template>
<div class="calendar" v-bind="calendarProps">
<div class="calendar-header">
<button class="calendar-header-button" v-bind="previousButtonProps">
&lt;
</button>
<span v-bind="gridLabelProps">
{{ gridLabel }}
</span>
<button class="calendar-header-button" v-bind="nextButtonProps">
&gt;
</button>
</div>
<div
v-if="currentView.type === 'weeks'"
v-bind="gridProps"
class="calendar-grid weeks-grid"
>
<div v-for="day in currentView.weekDays" :key="day" class="weekday-label">
{{ day }}
</div>
<CalendarCell
v-for="day in currentView.days"
v-bind="day"
class="calendar-cell"
:class="{
'outside-month': day.isOutsideMonth,
today: day.isToday,
}"
>
{{ day.label }}
</CalendarCell>
</div>
<div
v-if="currentView.type === 'months'"
v-bind="gridProps"
class="calendar-grid months-grid"
>
<CalendarCell
v-for="month in currentView.months"
v-bind="month"
class="calendar-cell"
>
{{ month.label }}
</CalendarCell>
</div>
<div
v-if="currentView.type === 'years'"
v-bind="gridProps"
class="calendar-grid years-grid"
>
<CalendarCell
v-for="year in currentView.years"
v-bind="year"
class="calendar-cell"
>
{{ year.label }}
</CalendarCell>
</div>
</div>
</template>
<style>
.calendar {
background-color: #fff;
}
.calendar-header {
display: flex;
align-items: center;
justify-content: space-between;
color: #000;
}
.calendar-grid {
display: grid;
gap: 8px;
}
.weeks-grid {
grid-template-columns: repeat(7, 1fr);
}
.weekday-label {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-weight: 500;
color: rgba(0, 0, 0, 0.6);
margin-top: 10px;
}
.calendar-cell {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border: 2px solid transparent;
color: #000;
width: 35px;
height: 35px;
border-radius: 999999px;
}
.calendar-cell:focus {
border-color: #059669;
outline: none;
}
.calendar-cell[aria-disabled='true'] {
cursor: not-allowed;
opacity: 0.5;
}
.calendar-cell[aria-selected='true'] {
background-color: #059669;
font-weight: 500;
color: white;
}
.calendar-cell[aria-disabled='true'] {
opacity: 0.1;
cursor: not-allowed;
}
.outside-month {
opacity: 0.4;
}
.months-grid,
.years-grid {
margin-top: 10px;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
.calendar-cell {
padding: 2px 4px;
width: auto;
height: unset;
}
}
.calendar-header-button {
background-color: transparent;
border: none;
cursor: pointer;
font-size: 20px;
border-radius: 6px;
&:hover {
background-color: #f0f0f0;
}
}
.today {
border-color: #059669;
}
</style>

Note that Formwerk already exposes a CalendarCell component that renders a span by default for convenience. It provides handling for user interactions and accessibility attributes.

You can build your own calendar cell component and customize it as needed with useCalendarCell composable.

<script setup lang="ts">
import { useCalendarCell, type CalendarCellProps } from '@formwerk/core';
const props = defineProps<CalendarCellProps>();
const { cellProps, label } = useCalendarCell(props);
</script>
<template>
<div v-bind="cellProps">
{{ label }}
</div>
</template>

You can use the following properties to validate the calendar field with HTML constraint validation:

NameTypeDescription
minDateThe minimum date that can be entered.
maxDateThe maximum date that can be entered.
requiredbooleanWhether the date field is required.

Use disabled to mark calendar as non-interactive. Disabled calendars are not validated and are not submitted.

If you need to prevent the user from interacting with the calendar while still allowing it to submit, consider using readonly instead.

<script setup lang="ts">
import Calendar from './Calendar.vue';
</script>
<template>
<Calendar label="Disabled Calendar" disabled />
</template>

Readonly calendars are validated and submitted, but they do not accept user input. The calendar is still focusable. For more info, check the MDN.

<script setup lang="ts">
import Calendar from './Calendar.vue';
</script>
<template>
<Calendar label="Readonly Calendar" readonly />
</template>

The calendar prop can be used to specify which calendar to use, along with the locale prop to set the locale of the calendar.

You can use @internationalized/date to create calendar objects. In this example, we will use the islamic-umalqura calendar along with the ar-SA locale.

<script setup lang="ts">
import {
createCalendar,
IslamicUmalquraCalendar,
} from '@internationalized/date';
import Calendar from './Calendar.vue';
// This will import all calendars available.
// Resulting in a larger bundle size.
const calendar = createCalendar('islamic-umalqura');
// This will import only the Islamic Umalqura calendar.
// This allows tree-shaking, reducing the bundle size.
// const calendar = IslamicUmalquraCalendar;
</script>
<template>
<Calendar label="Calendar" :calendar="calendar" locale="ar-SA" />
</template>

You can pass min and max props to set the minimum and maximum dates that can be selected. You will still need to handle styling those dates, out of range dates will be marked with aria-disabled attribute.

<script setup lang="ts">
import Calendar from './Calendar.vue';
// You need to be careful with the time component of the date object.
// JS date objects fills the date time with the current time component.
const min = new Date(2025, 0, 4, 0, 0, 0, 0);
const value = new Date('2025-01-15');
const max = new Date('2025-01-20');
</script>
<template>
<Calendar label="Calendar" :value="value" :min="min" :max="max" />
</template>

You can use the Calendar component as a picker which can be useful to pair with a date field.

To do that, you can use the usePicker composable:

<script setup lang="ts">
import { ref } from 'vue';
import { usePicker } from '@formwerk/core';
import Calendar from './Calendar.vue';
const { pickerProps, pickerTriggerProps } = usePicker({
label: 'Pick a date',
});
const date = ref();
</script>
<template>
<pre>Selected date: {{ date || 'none' }}</pre>
<button v-bind="pickerTriggerProps">Open Calendar</button>
<div v-bind="pickerProps" popover>
<Calendar label="Calendar" v-model="date" />
</div>
</template>

In that example, we are using the popover API, but you can use any other floating UI solution you prefer.

The calendar will have the 3 views enabled by default. You switch between the views by clicking the header of the calendar.

While we do not recommend disabling the views as your users may expect being able to navigate with them, you can still allow specific views by passing the allowedViews prop.

In the following example, we are disabling the year view:

<script setup lang="ts">
import Calendar from './Calendar.vue';
</script>
<template>
<Calendar label="Calendar" :allowed-views="['weeks', 'months']" />
</template>

Now if you click the header of the calendar, you will get the months view, if you click again you will remain on the months view rather than going to the years view.

These are the properties that can be passed to the useCalendar composable.

NameTypeDescription

These are the properties in the object returned by the useCalendar composable.

NameTypeDescription
calendarProps
Ref<{ role: string; dir: Direction; onKeydown(e: KeyboardEvent): void; id: string; }>
controlId
currentView
Ref<CalendarWeeksView | CalendarMonthsView | CalendarYearsView>
field
focusedDate
Ref<_internationalized_date197.ZonedDateTime>
gridLabel
gridLabelProps
Ref<{ 'aria-live': "polite"; tabindex: string; onClick: () =>
gridProps
Ref<{ 'aria-label'?: string; 'aria-labelledby'?: string; id: string; role: string; }>
nextButtonProps
Ref<{ [x: string]: unknown; type: "button"; role: string; tabindex: string; }>
previousButtonProps
Ref<{ [x: string]: unknown; type: "button"; role: string; tabindex: string; }>
selectedDate
Ref<_internationalized_date197.ZonedDateTime>
setView

These are the properties that can be passed to the useCalendarCell composable.

NameTypeDescription
disabled
focused
label
selected
type
value

These are the properties in the object returned by the useCalendarCell composable.

NameTypeDescription
cellProps
key
label