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
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
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
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
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>
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:
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
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.
Name | Type | Description |
---|---|---|
allowedViews | The available views to switch to and from in the calendar. | |
calendar | The calendar type to use for the calendar, e.g. `gregory`, `islamic-umalqura`, etc. | |
disabled | Whether the calendar is disabled. | |
field | The form field to use for the calendar. | |
label | The label for the calendar. | |
locale | The locale to use for the calendar. | |
max | The maximum date to use for the calendar. | |
min | The minimum date to use for the calendar. | |
modelValue | The current date to use for the calendar. | |
monthFormat | The format option for the month. | |
name | The field name of the calendar. | |
nextMonthButtonLabel | The label for the next month button. | |
previousMonthButtonLabel | The label for the previous month button. | |
readonly | Whether the calendar is readonly. | |
required | Whether the calendar is required. | |
schema | The schema to use for the calendar. | |
timeZone | The time zone to use for the calendar. | |
value | The initial value to use for the calendar. | |
weekDayFormat | The format option for the days of the week. | |
yearFormat | The format option for the year. |
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; }> | The props for the calendar element. |
currentView | Ref<CalendarWeeksView | CalendarMonthsView | CalendarYearsView> | The current view. |
displayError | Display the error message for the field. | |
errorMessage | The error message for the field. | |
errors | The errors for the field. | |
fieldValue | The value of the field. | |
focusedDate | The focused date. | |
gridLabel | The 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. |
isDirty | Whether the field is dirty. | |
isDisabled | Whether the field is disabled. | |
isTouched | Whether the field is touched. | |
isValid | Whether 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. |
selectedDate | The current date. | |
setErrors | (messages: Arrayable<string>) => void | Sets the errors for the field. |
setTouched | Sets the touched state for the field. | |
setValue | Sets the value for the field. | |
setView | Switches the current view (e.g: weeks, months, years) | |
submitErrorMessage | The error message for the field from the last submit attempt. | |
submitErrors | The 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.
Name | Type | Description |
---|---|---|
disabled | ||
focused | ||
label | ||
selected | ||
type | ||
value |
Returns
These are the properties in the object returned by the useCalendarCell
composable.
Name | Type | Description |
---|---|---|
cellProps | ||
key | ||
label |