Date Input
Form-associated single-date input with native picker, constraints, helper text and error states.
c2-date-input wraps the browser’s native date control in a consistent, themeable field. Its value, min and max are local-calendar ISO dates (YYYY-MM-DD), while form submission, reset, required validation, keyboard editing and the platform date picker remain native.
Import
import '@c2n/components/date-input'Usage
Listen for input while the value changes and change when it is committed. Call showPicker() from a user gesture to open the platform picker programmatically; valueAsDate exposes the selected value as a Date.
A themed calendar
The browser’s date picker cannot be styled and looks different in every browser. picker="calendar" replaces it with
a calendar popover drawn by the library: the calendar button, or Alt+ArrowDown in the field, opens one month of
c2-date-selector with Today and Clear under it, bounded by min and max. Picking a
day stores it, fires input and change, closes the popover and returns to the field; Escape or a press outside
closes it without a change. The date can still be typed.
The calendar and its popover are loaded the first time one opens, so a form of date fields that never opens one stays
as small as with the native picker. The popover is the calendar part and follows the
--c2-date-input__calendar--* variables; Today and Clear follow --c2-date-input__calendar-action--*, and their
text is the today and clear messages.
Presets are buttons of your own in the calendar-actions slot, beside Today and Clear. Call pick(date) from them:
it stores the date, fires input and change and closes the popover, as a day of the calendar does. showCalendar()
and hideCalendar() open and close the popover from script.
<c2-date-input id="due" picker="calendar" aria-label="Due date">
<c2-button slot="calendar-actions" data-days="7">In a week</c2-button>
</c2-date-input>const due = document.querySelector<DateInput>('#due')!
due.querySelector('[data-days]')!.addEventListener('click', (event) => {
due.pick(isoDaysFromToday(Number((event.currentTarget as HTMLElement).dataset.days)))
})The field is still the browser’s date input, so what the browser itself opens from it stays: on a phone a tap in the
field opens the browser’s picker, and so does a click in the field in Firefox. The calendar button and Alt+ArrowDown
open the themed one. step is not drawn in the calendar: a day off the step can be picked and makes the field
invalid, as a typed one does. If the calendar cannot be loaded, the button falls back to the browser’s picker.
Limiting the date range
min and max are inclusive bounds. The platform picker greys out every date outside them, and a date typed outside the range makes the field invalid (validity.rangeUnderflow / rangeOverflow), which blocks form submission. For a range relative to today, compute the bounds when the page loads — the examples below do so from data-min-days / data-max-days offsets:
The bounds are local-calendar YYYY-MM-DD strings, so build them from the local date parts rather than toISOString(), which is UTC and can land on the wrong day:
import type { DateInput } from '@c2n/components/date-input'
function isoDaysFromToday(offset: number) {
const date = new Date()
date.setDate(date.getDate() + offset)
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${date.getFullYear()}-${month}-${day}`
}
const serviceDate = document.querySelector<DateInput>('#service-date')!
serviceDate.min = isoDaysFromToday(-2) // d-2 and later
const birthDate = document.querySelector<DateInput>('#birth-date')!
birthDate.max = isoDaysFromToday(-1) // d-1 and earlier: past dates only
// Surface a typed out-of-range date as an error message.
serviceDate.addEventListener('change', async () => {
await serviceDate.updateComplete
serviceDate.error = !serviceDate.checkValidity()
serviceDate.errorText = serviceDate.validationMessage
})