Date picker
Summary
A calendar popover anchored to a text input. Clicking the
input opens a 6×7 month grid; the user picks a day and the
input's value is filled in ISO YYYY-MM-DD format.
Hand-rolled: no jQuery UI, no third-party calendar library.
Auto-binds on DOMContentLoaded for any input
with the .neura-date-picker-input marker class.
Fully keyboard-accessible: the input stays typeable while the popover is open; ↓ moves focus into the day grid, arrows move by day/week, Home/End jump to week edges, PageUp/PageDown change month (Shift: year), Enter selects, and Esc closes and returns focus to the input. The popover also closes on an outside click and when the page scrolls.
When to use
| Pattern | Use it for |
|---|---|
Auto-init.neura-date-picker-input | Static forms: add the marker class to a text input and the picker binds on load. |
ImperativeNeura.datePicker(input, opts) | Dynamic forms or when you need to listen for change events / set values from JS. |
US conventionfirstDayOfWeek: 0 | Sunday-first calendar layout. Default is Monday-first (ISO 8601). |
Examples
Click any input to open the calendar popover. Each demo binds with a different option set so you can verify min/max bounds, locales, time picker, and the paired-range behaviour.
Default - Monday-first
With initial value
US convention - Sunday-first
Listening for change
Min / max bounds
Locale - French
Date + time
Date range
Keyboard navigation
All pickers support keyboard nav when the popover is open and focus is in the day grid:
| Key | Action |
|---|---|
| ← / → | Previous / next day |
| ↑ / ↓ | Same day previous / next week |
| Home / End | Start / end of the focused week |
| PgUp / PgDn | Previous / next month |
| Shift+PgUp / PgDn | Previous / next year |
| Enter / Space | Select the focused day |
| Esc | Close the popover |
HTML
Auto-init: add the marker class on any <input type="text">. No JS needed:
<input type="text"
class="neura-field-text neura-date-picker-input"
placeholder="YYYY-MM-DD">
Pre-populate the input (value must be ISO YYYY-MM-DD):
<input type="text"
class="neura-field-text neura-date-picker-input"
value="2024-03-12">
Inside a Neura form group:
<form class="neura-form">
<div class="neura-field-group">
<label for="dob">Birth date</label>
<input type="text" id="dob"
class="neura-field-text neura-date-picker-input"
placeholder="YYYY-MM-DD">
<div class="neura-field-description">Click to pick from a calendar.</div>
</div>
</form>
CSS classes
| Class | Effect |
|---|---|
.neura-date-picker-input | Marker on the text input. Triggers auto-init binding. |
.neura-date-picker | Popover container (rendered by JS, not authored manually). |
.neura-date-picker-header | Top row: month nav + title. |
.neura-date-picker-prev / .neura-date-picker-next | Prev / next month buttons. |
.neura-date-picker-title | "March 2025" title between the nav buttons. |
.neura-date-picker-grid | The 6×7 day grid <table>. |
.neura-date-picker-day | Each clickable day cell. Carries its date as data-date="YYYY-MM-DD". |
.is-today / .is-selected / .is-other-month | State classes on day cells: today's date, the selected value, and leading/trailing days of the neighbouring months. |
.neura-date-picker-time | Time row (rendered only with time: true); holds the .neura-date-picker-hour and .neura-date-picker-minute selects. |
.neura-date-picker-footer | Today / Clear buttons. |
JavaScript
Constructor + options
| Member | Description |
|---|---|
Neura.datePicker(input, opts?) | Get or create the singleton picker for an input element / selector. |
opts.firstDayOfWeek | 0 = Sunday, 1 = Monday (default). When this option is not set, falls back to the first day of the effective locale: opts.locale if supplied, otherwise the global Neura.i18n locale. |
opts.locale | BCP-47 string (e.g. 'fr-FR', 'ja-JP'). Localizes month and day names via Intl.DateTimeFormat. |
opts.min | Date or ISO string. Days before this render disabled and ignore clicks. |
opts.max | Date or ISO string. Days after this render disabled and ignore clicks. |
opts.time | true to enable a 24h HH:MM picker row. Output format becomes YYYY-MM-DDTHH:MM. |
opts.timeStep | Minute granularity for the time row. Default 15. |
opts.format | Output format is fixed by time: ISO YYYY-MM-DD, or YYYY-MM-DDTHH:MM when time is set. Custom format strings are not supported in v0.1. |
Instance methods
| Member | Description |
|---|---|
.show() | Open the calendar popover. Never steals focus; the input stays typeable. |
.hide() | Close it. |
.focusGrid() | Move keyboard focus to the focused day cell in the grid (what ↓ in the input does). |
.setValue(date) | Set the value. Accepts a Date, an ISO string, or null to clear. |
.getValue() | Returns the current value as a Date instance, or null when empty. |
.setMin(date) | Update the minimum bound at runtime. Re-renders the grid. |
.setMax(date) | Update the maximum bound at runtime. |
.on('change', fn) | Fires when the user picks a date or clears the input. e.detail is { value, date }; value is '' when cleared. |
.off('change', fn) | Unsubscribe. |
.destroy() | Tear down listeners and remove the popover from the DOM. |
All methods return the instance, so calls chain. The underlying
DOM event is a bubbling neura-date-picker-change
CustomEvent dispatched on the input, usable directly with
addEventListener for delegated listening.
Date range API
| Member | Description |
|---|---|
Neura.dateRange(startInput, endInput, opts?) | Pair two pickers. Start's value becomes end's min, end's becomes start's max. Returns a façade with the members below. |
.start / .end | The two underlying DatePicker instances. Use them for any per-side operation. |
.getValue() | Returns { start: Date | null, end: Date | null }. |
.on('change', fn) | Fires when either side changes. e.detail is { start, end }. (DOM event: a bubbling neura-date-range-change CustomEvent on the start input.) |
.off('change', fn) | Unsubscribe. |
.destroy() | Destroy both inner pickers and remove listeners. |
Listen for date changes:
<input type="text" id="dob" class="neura-field-text neura-date-picker-input" placeholder="YYYY-MM-DD">
<script>
Neura.datePicker('#dob').on('change', (e) => {
console.log('picked:', e.detail.value); // 'YYYY-MM-DD' or '' when cleared
console.log('as Date:', e.detail.date); // Date object or undefined
});
</script>
US convention (Sunday-first):
Neura.datePicker('#meeting', { firstDayOfWeek: 0 });
Set / read the value programmatically:
const dp = Neura.datePicker('#dob');
// Set from a Date object
dp.setValue(new Date(2025, 2, 15)); // → input becomes "2025-03-15"
// Set from an ISO string
dp.setValue('2025-03-15');
// Read back
const date = dp.getValue(); // → Date instance or null
// Clear
dp.setValue(null);
Re-bind a date picker after rendering an input dynamically:
// Fields rendered after page load won't have been touched by auto-init.
// Re-scan the document, or just instantiate explicitly:
import('@neura/js/components/date-picker.js').then((m) => m.autoInit());
// Or, for a single known input:
Neura.datePicker(document.querySelector('#new-row .neura-date-picker-input'));
For custom validation on submit, read the parsed value:
document.getElementById('signup').addEventListener('submit', (e) => {
const date = Neura.datePicker('#dob').getValue();
if (!date) {
e.preventDefault();
alert('Please pick a date');
return;
}
if (date.getFullYear() < 1900) {
e.preventDefault();
alert('Invalid year');
}
});
Min / max bounds
Set min / max at construction time. Both accept a Date or an ISO string:
Neura.datePicker('#dob', {
min: '1900-01-01', // ISO string
max: new Date(), // Date instance - not in the future
});
For relative bounds (e.g. only the last 30 days, or the next 90 days), compute the Date with arithmetic:
// Today minus 30 days, up to today
const min = new Date();
min.setDate(min.getDate() - 30);
Neura.datePicker('#expense-date', { min, max: new Date() });
// Today through 90 days from now
const max = new Date();
max.setDate(max.getDate() + 90);
Neura.datePicker('#delivery', { min: new Date(), max });
Note: setDate() handles month and year roll-over automatically, so subtracting 30 days from March 5 correctly lands in February (or January, etc.).
Update bounds at runtime, useful when one input depends on another:
const arrival = Neura.datePicker('#arrival');
const departure = Neura.datePicker('#departure');
arrival.on('change', (e) => {
departure.setMin(e.detail.date || null);
});
Out-of-range days render with the native disabled
attribute and aria-disabled="true". Prev/next month
buttons disable themselves automatically when the entire
neighbouring month is out of range, and the Today footer button
is a no-op while today is out of range.
Locales
Pass any BCP-47 locale string. Neura uses
Intl.DateTimeFormat for month and day names, and
Intl.Locale().getWeekInfo() for the locale's first
day of week (with Monday as fallback for older browsers).
Neura.datePicker('#dob', { locale: 'fr-FR' }); // janvier · lun., mar., …
Neura.datePicker('#dob', { locale: 'ja-JP' }); // 1月 · 月, 火, …
Neura.datePicker('#dob', { locale: 'en-US' }); // English, Sunday-first
Override the locale's first-day-of-week:
// Use French labels but force Monday-first regardless of the locale's default:
Neura.datePicker('#dob', { locale: 'fr-FR', firstDayOfWeek: 1 });
Date + time
Set time: true to enable a 24h HH:MM
picker row below the calendar. The output format becomes ISO-8601
YYYY-MM-DDTHH:MM and the popover stays open after a
day is picked so the user can still tweak the time:
Neura.datePicker('#reservation', {
time: true,
timeStep: 30, // half-hour granularity (default is 15)
});
When parsing input values, both YYYY-MM-DD and
YYYY-MM-DDTHH:MM are accepted, so an input pre-filled
with a date-only value works fine and the time defaults to
midnight.
Date range
Pair two date inputs so picking the start automatically constrains the end (and vice versa):
<input type="text" id="trip-start" class="neura-field-text">
<input type="text" id="trip-end" class="neura-field-text">
<script>
const range = Neura.dateRange('#trip-start', '#trip-end');
range.on('change', (e) => {
console.log('range:', e.detail.start, '→', e.detail.end);
});
</script>
Both inputs receive their own date-picker instance, accessible
via range.start and range.end if you
need to call show(), listen to per-side events, etc.
Pass opts to apply the same configuration
(locale, time, outer min/max)
to both pickers.
Keyboard navigation
When the popover is open and focus is in the grid, the arrow keys
move through days, PgUp/PgDn change month,
Home/End jump within the focused week,
Enter selects, and Esc closes. The full
mapping is in the Examples section above. Out-of-range days are
skipped; the focus halts at a disabled cell rather than landing
on it. Under dir="rtl" the ←/→
keys follow visual direction (← moves forward a day);
the other keys stay temporal.
AUI compatibility
The legacy AJS.DatePicker entry point is routed
through the shim to Neura.datePicker, so existing
Velocity templates and plugins that call
AJS.DatePicker(input, opts) continue to work.
Inputs marked with the legacy .aui-date-picker
class are picked up by the shim's JS auto-init and bound to
the Neura date picker.