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.

Features

  • 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.

Keyboard Features

Week view

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.

Month view

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.

Year view

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.

Anatomy

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)

Building a Calendar Component

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>
96 collapsed lines
<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>

Validation

HTML Constraints

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.

Standard Schema

Usage

Disabled

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

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>

Locale and Calendar Support

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>

Min and Max

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>

Calendar as a picker

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.

Disabling calendar views

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.

API

useCalendar

Props

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

NameTypeDescription
allowedViewsThe available views to switch to and from in the calendar.
calendarThe calendar type to use for the calendar, e.g. `gregory`, `islamic-umalqura`, etc.
disabledWhether the calendar is disabled.
fieldThe form field to use for the calendar.
labelThe label for the calendar.
localeThe locale to use for the calendar.
maxThe maximum date to use for the calendar.
minThe minimum date to use for the calendar.
modelValueThe current date to use for the calendar.
monthFormatThe format option for the month.
nameThe field name of the calendar.
nextMonthButtonLabelThe label for the next month button.
previousMonthButtonLabelThe label for the previous month button.
readonlyWhether the calendar is readonly.
requiredWhether the calendar is required.
schemaThe schema to use for the calendar.
timeZoneThe time zone to use for the calendar.
valueThe initial value to use for the calendar.
weekDayFormatThe format option for the days of the week.
yearFormatThe format option for the year.

Returns

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; }>
The props for the calendar element.
currentView
Ref<CalendarWeeksView | CalendarMonthsView | CalendarYearsView>
The current view.
displayErrorDisplay the error message for the field.
errorMessageThe error message for the field.
errorsThe errors for the field.
fieldValueThe value of the field.
focusedDateThe focused date.
gridLabelThe label for the current panel. If it is a day panel, the label will be the month and year. If it is a month panel, the label will be the year. If it is a year panel, the label will be the range of years currently being displayed.
gridLabelProps
Ref<{ 'aria-live': "polite"; tabindex: string; onClick: () =>
The props for the panel label element.
gridProps
Ref<{ 'aria-label'?: string; 'aria-labelledby'?: string; id: string; role: string; }>
The props for the grid element that displays the panel values.
isDirtyWhether the field is dirty.
isDisabledWhether the field is disabled.
isTouchedWhether the field is touched.
isValidWhether the field is valid.
nextButtonProps
Ref<{ [x: string]: unknown; type: "button"; role: string; tabindex: string; }>
The props for the next panel values button. if it is a day panel, the button will move the panel to the next month. If it is a month panel, the button will move the panel to the next year. If it is a year panel, the button will move the panel to the next set of years.
previousButtonProps
Ref<{ [x: string]: unknown; type: "button"; role: string; tabindex: string; }>
The props for the previous panel values button. If it is a day panel, the button will move the panel to the previous month. If it is a month panel, the button will move the panel to the previous year. If it is a year panel, the button will move the panel to the previous set of years.
selectedDateThe current date.
setErrors
(messages: Arrayable<string>) => void
Sets the errors for the field.
setTouchedSets the touched state for the field.
setValueSets the value for the field.
setViewSwitches the current view (e.g: weeks, months, years)
submitErrorMessageThe error message for the field from the last submit attempt.
submitErrorsThe errors for the field from the last submit attempt.
validate
(mutate?: boolean) => Promise<ValidationResult<unknown>>
Validates the field.

useCalendarCell

Props

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

NameTypeDescription
disabled
focused
label
selected
type
value

Returns

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

NameTypeDescription
cellProps
key
label