Skip to content

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

  • 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

KeyDescription
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

Label
Label
09
Date segment
/
16
/
2025
Control Group
Help text
Description or Error Message

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 DateField from './DateField.vue';
</script>
<template>
<DateField name="date" label="Date" />
</template>
<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>
84 collapsed lines
<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

HTML Constraints

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

NameTypeDescription
minDateThe minimum date that can be entered.
maxDateThe maximum date that can be entered.
requiredbooleanWhether 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

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

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>

Usage

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

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

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

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

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

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 DateField from './DateField.vue';
</script>
<template>
<DateField label="Date" />
</template>
<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>
103 collapsed lines
<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>
<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>

API

useDateField

Props

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

NameTypeDescription
calendarThe calendar type to use for the field, e.g. `gregory`, `islamic-umalqura`, etc.
descriptionThe description to use for the field.
disabledWhether the field is disabled.
formatOptionsThe Intl.DateTimeFormatOptions to use for the field, used to format the date value.
labelThe label to use for the field.
localeThe locale to use for the field.
maxThe maximum date to use for the field.
minThe minimum date to use for the field.
modelValueThe model value to use for the field.
nameThe name to use for the field.
placeholderThe placeholder to use for the field.
readonlyWhether the field is readonly.
requiredWhether the field is required.
schemaThe schema to use for the field.
timeZoneThe time zone to use for the field, e.g. `UTC`, `America/New_York`, etc.
valueThe value to use for the field.

Returns

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

NameTypeDescription
calendarPropsThe 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.
descriptionPropsThe props to use for the description element.
directionThe direction of the field.
displayErrorDisplay the error message for the field.
errorMessageThe error message for the field.
errorMessageProps
Ref<{ 'aria-live': "polite"; 'aria-atomic': boolean; id: string; }>
The props to use for the error message element.
errorsThe errors for the field.
fieldValueThe value of the field.
isDirtyWhether the field is dirty.
isDisabledWhether the field is disabled.
isTouchedWhether the field is touched.
isValidWhether the field is valid.
labelPropsThe props to use for the label element.
segmentsThe datetime segments, you need to render these with the `DateTimeSegment` component.
setErrors
(messages: Arrayable<string>) => void
Sets the errors for the field.
setTouchedSets the touched state for the field.
setValueSets the value for the field.
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.

useDateSegment

Props

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

NameTypeDescription
disabledWhether the segment is disabled.
readonlyWhether the segment is readonly.
spinOnlyForces the segment to behave strictly as a spin button, preventing any other interactions like input events. Useful for time fields and specific UX needs.
typeThe type of the segment.
valueThe text value of the segment.

Returns

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

NameTypeDescription
key
segmentProps