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
Section titled “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-modelbinding. - Comprehensive keyboard shortcuts support.
Keyboard Features
Section titled “Keyboard Features”Week view
Section titled “Week view”| Key | Description |
|---|---|
| ↓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
Section titled “Month view”| Key | Description |
|---|---|
| ↓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
Section titled “Year view”| Key | Description |
|---|---|
| ↓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
Section titled “Anatomy”Building a Calendar Component
Section titled “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 { 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"> < </button>
<span v-bind="gridLabelProps"> {{ gridLabel }} </span>
<button class="calendar-header-button" v-bind="nextButtonProps"> > </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>Validation
Section titled “Validation”HTML Constraints
Section titled “HTML Constraints”You can use the following properties to validate the calendar field with HTML constraint validation:
| Name | Type | Description |
|---|---|---|
min | Date | The minimum date that can be entered. |
max | Date | The maximum date that can be entered. |
required | boolean | Whether the date field is required. |
Standard Schema
Section titled “Standard Schema”Disabled
Section titled “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
Section titled “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
Section titled “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
Section titled “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
Section titled “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
Section titled “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.
useCalendar
Section titled “useCalendar”These are the properties that can be passed to the useCalendar composable.
| Name | Type | Description |
|---|
Returns
Section titled “Returns”These are the properties in the object returned by the useCalendar composable.
| Name | Type | Description |
|---|---|---|
| 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 |
useCalendarCell
Section titled “useCalendarCell”These are the properties that can be passed to the useCalendarCell composable.
| Name | Type | Description |
|---|---|---|
| disabled | ||
| focused | ||
| label | ||
| selected | ||
| type | ||
| value |
Returns
Section titled “Returns”These are the properties in the object returned by the useCalendarCell composable.
| Name | Type | Description |
|---|---|---|
| cellProps | ||
| key | ||
| label |