Date Fields
Date fields are a type of input field that allows users to enter a date. They are a common feature in many web applications, and Formwerk provides a useDateTimeField
composable that can be used to create date fields in your application.
Features
Section titled “Features”- Labeling, Descriptions, and error messages are automatically linked to the date field elements.
- 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.
- Focus management and auto navigation for date segments.
- Support for
v-model
binding. - Comprehensive keyboard shortcuts support.
Keyboard Features
Section titled “Keyboard Features”Key | Description |
---|---|
↓ArrowDown | Decrements selected segment by 1. |
↑ArrowUp | Increments selected segment by 1. |
←ArrowLeft | Moves the focus to the previous segment. |
→ArrowRight | Moves the focus to the next segment. |
⌦Delete | Clears the current segment. |
⌫Backspace | Clears the current segment. |
⇥Tab | Moves the focus to the next segment or next element in the tab index order if it is the last segment. |
Anatomy
Section titled “Anatomy”Building a Date Field Component
Section titled “Building a Date Field Component”Just like the native HTML date field, the date field in Formwork is compromised of date segments, each segment represents a part of the date/time value (e.g: day, month, year, hour, minute, etc).
The segments are generated for you automatically based on the formatOptions
you pass to the useDateTimeField
composable. You will need to bind the prop objects returned by the composable to the corresponding DOM elements in the anatomy.
If you do not pass the formatOptions
prop, the date field will use the default format options for the locale of your app.
<script setup lang="ts">import { useDateField, type DateFieldProps, DateTimeSegment,} from '@formwerk/core';
const props = defineProps<DateFieldProps>();
const { controlProps, isTouched, labelProps, errorMessageProps, errorMessage, segments, direction,} = useDateField(props);</script>
<template> <div class="InputDate" :class="{ touched: isTouched }" :dir="direction"> <span class="label">{{ label }}</span>
<div class="control"> <div v-bind="controlProps"> <DateTimeSegment v-for="segment in segments" v-bind="segment" class="segment" /> </div> </div>
<span class="error-message"> {{ errorMessage }} </span> </div></template>
<style scoped>.InputDate { position: relative; width: 100%; margin-bottom: 1.5em;
--color-primary: #10b981; --color-text-primary: #333; --color-text-secondary: #666; --color-border: #d4d4d8; --color-focus: var(--color-primary); --color-error: #f00; --color-background: #fff;}
.label { display: block; margin-bottom: 0.25rem; font-size: 14px; font-weight: 500; color: var(--color-text-primary);}
.control { display: flex; align-items: center; gap: 0.25em; width: max-content; padding: 0.5rem 0.6rem; border: 1px solid var(--color-border); border-radius: 6px; background-color: var(--color-background); color: var(--color-text-primary); font-size: 13px; transition: all 0.3s ease;}
.control:focus-within { border-color: var(--color-focus); box-shadow: 0 0 0 1px var(--color-focus); outline: none;}
.segment { padding: 0.125rem; border-radius: 0.25rem; caret-color: transparent;
&:focus { background-color: var(--color-focus); color: #fff; outline: none; }
&[aria-disabled='true'] { opacity: 0.4; }}
.error-message { display: none; position: absolute; left: 0; margin-top: 0.25rem; font-size: 13px; color: var(--color-error);}
.InputDate:has(:user-invalid),.InputDate:has(.error-message:not(:empty)) { --color-border: var(--color-error); --color-focus: var(--color-error);}
.InputDate:has(:user-invalid) .error-message,.InputDate:has(.error-message:not(:empty)) .error-message { display: block;}
.InputDate:has(:disabled) .control { cursor: not-allowed; opacity: 0.7;}</style>
The DateTimeSegment
component is used to render each segment of the date field. We provide one for convenience that you can use. You can also build your own with useDateTimeSegment
composable.
<script setup lang="ts">import { useDateTimeSegment, type DateTimeSegmentProps } from '@formwerk/core';
const props = defineProps<DateTimeSegmentProps>();
const { segmentProps, label } = useDateTimeSegment(props);</script>
<template> <div v-bind="segmentProps"> {{ label }} </div></template>
Validation
Section titled “Validation”HTML Constraints
Section titled “HTML Constraints”You can use the following properties to validate the date field with native 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. |
<script setup lang="ts">import DateField from './DateField.vue';</script>
<template> <DateField label="HTML Constraints" required :min="new Date('2025-01-01')" :max="new Date('2025-12-31')" /></template>
Standard Schema
Section titled “Standard Schema”useDateTimeField
supports Standard Schema validation through the schema
prop. This includes multiple providers like Zod, Valibot, Arktype, and more.
In this example, we are validating that the date field is between January 1st, 2025 and December 31st, 2025.
<script setup lang="ts">import { z } from 'zod';import DateField from './DateField.vue';
const schema = z.date().min(new Date('2025-01-01')).max(new Date('2025-12-31'));</script>
<template> <DateField label="Standard Schema" :schema="schema" /></template>
Mixed Validation
Section titled “Mixed Validation”All date fields created with Formwerk support mixed validation, which means you can use both HTML constraints and Standard Schema validation to validate the field, and they work seamlessly together.
Note that HTML constraints are validated first, so any errors from the HTML constraints will be displayed first. Then, once all HTML constraints are satisfied, the Standard Schema is validated.
<script setup lang="ts">import { z } from 'zod';import DateField from './DateField.vue';
const schema = z.date().min(new Date('2025-01-01')).max(new Date('2025-12-31'));</script>
<template> <DateField label="Mixed Validation" :schema="schema" required /></template>
Disabled
Section titled “Disabled”Use the disabled
prop to disable the date field. Disabled date fields are not validated and are not submitted.
If you need to prevent the user from interacting with the field while still allowing it to submit, consider using readonly
instead.
<script setup lang="ts">import DateField from './DateField.vue';</script>
<template> <DateField label="Disabled" :value="new Date('2025-02-11')" disabled /></template>
Readonly
Section titled “Readonly”Use the readonly
prop to make the date field read-only. Read-only date fields are still validated and submitted.
<script setup lang="ts">import DateField from './DateField.vue';</script>
<template> <DateField label="Readonly" :value="new Date('2025-02-11')" readonly /></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 entered, not only they are used for validation but they affect the date segments that are available for selection and interaction.
<script setup lang="ts">import DateField from './DateField.vue';</script>
<template> <DateField label="Same Year and Month" :min="new Date('2025-02-01')" :max="new Date('2025-02-15')" />
<DateField label="Same Year" :min="new Date('2025-01-01')" :max="new Date('2025-12-31')" />
<DateField label="Different Year" :min="new Date('2024-01-01')" :max="new Date('2025-12-31')" /></template>
Notice in the previous examples, some parts of the date field are disabled.
This is because when providing both a min
and a max
, Formwerk checks the possibilities of the date segments, and if a segment has only one possible value then it automatically sets it and disables it. Just like the native input[type="date"]
element.
Format Options
Section titled “Format Options”You can pass any Intl.DateTimeFormatOptions
to the formatOptions
prop to customize the date field’s display format.
<script setup lang="ts">import DateField from './DateField.vue';</script>
<template> <DateField label="Format Options" :formatOptions="{ year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', hour12: true, }" /></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 date field.
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 DateField from './DateField.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> <DateField label="Hijri Date" :value="new Date('2025-02-11')" :calendar="calendar" locale="ar-SA" /></template>
Usage with Calendar Component
Section titled “Usage with Calendar Component”You can use the DateField
component with the Calendar
component to allow users to select a date. First check out the Calendar guide to build your own calendar component so that you can use it in the next example.
<script setup lang="ts">import { useDateField, DateTimeSegment, usePicker } from '@formwerk/core';import Calendar from './Calendar.vue';
import { type DateFieldProps } from '@formwerk/core';
const props = defineProps<DateFieldProps>();
const { controlProps, isTouched, labelProps, errorMessageProps, errorMessage, segments, direction, calendarProps,} = useDateField(props);
const { pickerProps, pickerTriggerProps } = usePicker({ label: 'Pick a date',});</script>
<template> <div class="InputDate" :class="{ touched: isTouched }" :dir="direction"> <span class="label">{{ label }}</span>
<div class="control"> <div v-bind="controlProps"> <DateTimeSegment v-for="segment in segments" v-bind="segment" class="segment" /> </div>
<button class="picker-trigger" v-bind="pickerTriggerProps"> <svg xmlns="http://www.w3.org/2000/svg" fill="#000000" viewBox="0 0 256 256" > <path d="M208,32H184V24a8,8,0,0,0-16,0v8H88V24a8,8,0,0,0-16,0v8H48A16,16,0,0,0,32,48V208a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V48A16,16,0,0,0,208,32ZM72,48v8a8,8,0,0,0,16,0V48h80v8a8,8,0,0,0,16,0V48h24V80H48V48ZM208,208H48V96H208V208Z" ></path> </svg> </button> </div> <div v-bind="pickerProps" popover> <Calendar v-bind="calendarProps" /> </div>
<span class="error-message"> {{ errorMessage }} </span> </div></template>
<style scoped>.InputDate { position: relative; width: 100%; margin-bottom: 1.5em;
--color-primary: #10b981; --color-text-primary: #333; --color-text-secondary: #666; --color-border: #d4d4d8; --color-focus: var(--color-primary); --color-error: #f00; --color-background: #fff;}
.label { display: block; margin-bottom: 0.25rem; font-size: 14px; font-weight: 500; color: var(--color-text-primary);}
.control { display: flex; align-items: center; gap: 0.25em; width: max-content; padding: 0.5rem 0.6rem; border: 1px solid var(--color-border); border-radius: 6px; background-color: var(--color-background); color: var(--color-text-primary); font-size: 13px; transition: all 0.3s ease;}
.control:focus-within { border-color: var(--color-focus); box-shadow: 0 0 0 1px var(--color-focus); outline: none;}
.segment { padding: 0.125rem; border-radius: 0.25rem; caret-color: transparent;}
.segment:focus { background-color: var(--color-focus); color: #fff; outline: none;}
.error-message { display: none; position: absolute; left: 0; margin-top: 0.25rem; font-size: 13px; color: var(--color-error);}
.picker-trigger { display: flex; align-items: center; justify-content: center; width: 2rem; height: 2rem; border: none; border-radius: 6px; background-color: transparent; cursor: pointer;
&:hover { background-color: var(--color-border); }}
[popover] { border: none; padding: 6px; border-radius: 6px; box-shadow: 0 0 0 1px var(--color-border);}
.InputDate:has(:user-invalid),.InputDate:has(.error-message:not(:empty)) { --color-border: var(--color-error); --color-focus: var(--color-error);}
.InputDate:has(:user-invalid) .error-message,.InputDate:has(.error-message:not(:empty)) .error-message { display: block;}
.InputDate:has(:disabled) .control { cursor: not-allowed; opacity: 0.7;}</style>
Examples
Section titled “Examples”These are some examples of date fields built with Formwerk.
useDateField
Section titled “useDateField”These are the properties that can be passed to the useDateField
composable.
Name | Type | Description |
---|---|---|
calendar | The calendar type to use for the field, e.g. `gregory`, `islamic-umalqura`, etc. | |
description | The description to use for the field. | |
disabled | Whether the field is disabled. | |
formatOptions | The Intl.DateTimeFormatOptions to use for the field, used to format the date value. | |
label | The label to use for the field. | |
locale | The locale to use for the field. | |
max | The maximum date to use for the field. | |
min | The minimum date to use for the field. | |
modelValue | The model value to use for the field. | |
name | The name to use for the field. | |
placeholder | The placeholder to use for the field. | |
readonly | Whether the field is readonly. | |
required | Whether the field is required. | |
schema | The schema to use for the field. | |
timeZone | The time zone to use for the field, e.g. `UTC`, `America/New_York`, etc. | |
value | The value to use for the field. |
Returns
Section titled “Returns”These are the properties in the object returned by the useDateField
composable.
Name | Type | Description |
---|---|---|
calendarProps | The props to use for the calendar composable/component. | |
controlProps | Ref<{ 'aria-disabled': true; 'aria-invalid': boolean; 'aria-errormessage': string; 'aria-describedby': string; 'aria-label'?: string; 'aria-labelledby'?: string; id: string; role: string; }> | The props to use for the control element. |
descriptionProps | The props to use for the description element. | |
direction | The direction of the field. | |
displayError | Display the error message for the field. | |
errorMessage | The error message for the field. | |
errorMessageProps | Ref<{ 'aria-live': "polite"; 'aria-atomic': boolean; id: string; }> | The props to use for the error message element. |
errors | The errors for the field. | |
fieldValue | The value of the field. | |
isDirty | Whether the field is dirty. | |
isDisabled | Whether the field is disabled. | |
isTouched | Whether the field is touched. | |
isValid | Whether the field is valid. | |
labelProps | The props to use for the label element. | |
segments | The datetime segments, you need to render these with the `DateTimeSegment` component. | |
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. | |
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. |
useDateSegment
Section titled “useDateSegment”These are the properties that can be passed to the useDateSegment
composable.
Name | Type | Description |
---|---|---|
disabled | Whether the segment is disabled. | |
readonly | Whether the segment is readonly. | |
spinOnly | Forces the segment to behave strictly as a spin button, preventing any other interactions like input events. Useful for time fields and specific UX needs. | |
type | The type of the segment. | |
value | The text value of the segment. |
Returns
Section titled “Returns”These are the properties in the object returned by the useDateTimeSegment
composable.
Name | Type | Description |
---|---|---|
key | ||
segmentProps |