A calendar date picker with single-date and date-range modes. Renders a text input that opens a floating calendar panel. Dates are represented as ISO 8601 strings (YYYY-MM-DD).
<x-date-picker></x-date-picker>
| Attribute | Type | Default | Description |
|---|---|---|---|
mode | enum | "single" | Selection mode: single range |
value | string | — | Selected date in YYYY-MM-DD format (single mode) |
start | string | — | Range start date in YYYY-MM-DD format (range mode) |
end | string | — | Range end date in YYYY-MM-DD format (range mode) |
min | string | — | Minimum selectable date (YYYY-MM-DD) |
max | string | — | Maximum selectable date (YYYY-MM-DD) |
format | enum | "iso" | Display format: iso (YYYY-MM-DD), localized (long month) or short (short month), the last two via Intl |
locale | string | — | BCP 47 locale tag for localized formatting (e.g. "en-US") |
separator | string | " - " | String placed between start and end in range display |
auto-swap | boolean | false | Automatically swap start/end when end is selected before start |
range-allow-same-day | boolean | false | Allow start and end to be the same date in range mode |
close-on-select | boolean | false | Close the calendar after a date is picked, a range is complete, or typed text is committed with Enter |
placeholder | string | — | Input placeholder text |
disabled | boolean | false | Disables the input and calendar |
readonly | boolean | false | Input is read-only; calendar can still open but not select |
required | boolean | false | Marks the field as required |
name | string | — | Form field name |
autocomplete | string | — | HTML autocomplete hint |
error | string | — | Inline validation message; shows the error part and marks the field invalid |
aria-label | string | — | Accessible label for the input |
aria-describedby | string | — | References a describing element |
| Property | Type | Reflects attribute |
|---|---|---|
mode | string | mode |
value | string | value |
start | string | start |
end | string | end |
disabled | boolean | disabled |
readOnly | boolean | readonly |
required | boolean | required |
open | boolean | open |
error | string | error |
name | string | name |
min | string | min |
max | string | max |
format | string | format |
locale | string | locale |
separator | string | separator |
placeholder | string | placeholder |
autocomplete | string | autocomplete |
autoSwap | boolean | auto-swap |
rangeAllowSameDay | boolean | range-allow-same-day |
closeOnSelect | boolean | close-on-select |
| Event | Cancelable | Detail | Description |
|---|---|---|---|
x-date-picker-input | no | { value, mode } | Fired on each user text input |
x-date-picker-change-request | yes | { value, date, start, end, mode, reason } | Fired before committing a date selection |
x-date-picker-change | no | { value, date, start, end, mode, reason } | Fired after the selection is committed |
reason is one of "click", "keyboard", "blur", or "programmatic".
Every key is always present and null where the current mode has nothing to say, so a handler null-checks rather than testing for a missing key. In single mode value holds the ISO date string. In range mode start and end hold them, and end is null while the range is open. On a range click they hold the range that the click asks for, and date holds the day clicked. date is null otherwise. After an accepted request, x-date-picker-change carries what the request asked.
A null field asks for no attribute, so a request asks for no attribute of the other mode. Give a picker only the attributes of its mode.
Deleting the text and committing (blur, Enter, or commit()) clears the committed value, exactly as clear() does. It fires a cancelable x-date-picker-change-request first, then x-date-picker-change, both with null in value, start and end. Cancelling the request keeps the committed value and restores the input text.
Committing a blank input when nothing is committed yet is a no-op: neither event fires.
The calendar shows the month of value, or of start in range mode. When that date changes, also when a page writes it, the calendar moves to its month. Otherwise it keeps the month the user browsed to. So in range mode, picking the end in a later month keeps that month. Opening the calendar shows the month of the date again.
Today is the local date of the user, from the local date fields of the clock. It is not the UTC date, so near midnight the calendar still opens on the day the user sees.
| Slot | Description |
|---|---|
| (default) | No default slot content expected |
btn part) has aria-label="Open calendar". Its calendar icon is an SVG drawn in currentColor with aria-hidden="true", so it takes the button text colour.role="combobox" with aria-expanded (synced to open state) and aria-haspopup="dialog".role="dialog" with aria-modal="true".aria-live="polite" for screen reader announcements.role="gridcell" with aria-selected and aria-disabled.aria-live="polite", aria-atomic="true") is provided.error attribute is set, the input gets aria-invalid="true" and its aria-describedby points at the error part (appended to any author-supplied aria-describedby). The error part is an assertive live region (role="alert") so screen readers announce the validation message.x-date-picker is a form-associated custom element (via ElementInternals). In a <form>:
name: the ISO date in single mode, or start/end once a range is complete.required picker with no committed date reports valueMissing and blocks submission; setting the error attribute reports a customError. This is what x-form and native submission gate on.form.reset() clears the committed value(s) (mirroring how x-form-field resets) and drops the error attribute, so a stale validation message does not survive the reset. <fieldset disabled> disables it via formDisabledCallback.Like a native form control, x-date-picker exposes read-only validity, validationMessage, willValidate, form and labels, plus checkValidity() and reportValidity(). All seven delegate to the element's ElementInternals, so el.checkValidity() answers the same question a submit attempt would.
| Key | Action |
|---|---|
ArrowLeft | Move one day back |
ArrowRight | Move one day forward |
ArrowUp | Move one week back |
ArrowDown | Move one week forward |
PageUp | Move one month back |
PageDown | Move one month forward |
Home | Go to first day of week |
End | Go to last day of week |
Enter/Space | Select focused date |
Escape | Close calendar without selecting |
| Click outside | Close calendar without selecting |
<x-date-picker placeholder="Select a date"></x-date-picker>
<x-date-picker mode="range" placeholder="Select date range"></x-date-picker>
<x-date-picker value="2026-03-18"></x-date-picker>
<x-date-picker min="2026-01-01" max="2026-12-31"></x-date-picker>
<x-date-picker format="localized" locale="en-US" placeholder="Pick a date"></x-date-picker>
format="short" shows the short month of the locale. With locale="en-GB" a range reads
27 Jul 2026 - 7 Aug 2026.
<x-date-picker mode="range" format="short" locale="en-GB"></x-date-picker>
<x-date-picker disabled placeholder="Not available"></x-date-picker>
The error attribute renders an inline message below the field and marks it
invalid. Inside x-form, this is driven for you by form.setFieldError(name, message).
<x-date-picker name="dob" error="Please enter a valid date"></x-date-picker>
Clear it by removing the attribute (or el.error = ''). Style the message via the
error CSS part or the --x-date-picker-error-color custom property.
document.querySelector('x-date-picker').addEventListener('x-date-picker-change', e => {
console.log('selected date:', e.detail.value);
});
[:x-date-picker {:placeholder "Select a date"
:on-x-date-picker-change
(fn [e] (swap! state assoc :date (.. e -detail -value)))}]
[:x-date-picker {:mode "range"
:placeholder "Select date range"
:on-x-date-picker-change
(fn [e]
(swap! state assoc
:start (.. e -detail -start)
:end (.. e -detail -end)))}]
Can you improve this documentation? These fine people already did:
Alexander van Elsas & Mark AddlemanEdit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |