Dates & Times
Having an easy-to-use widget to select dates and times greatly improves user experience.
On this page
Overview
Over the course of HQ's history, we have trialed and tested several date and time pickers. We have finally settled on one library, Tempus Dominus, for its accessibility features and ease of localization. Below are examples of how to use it in various scenarios, as well as guidance for how to replace older tools on Bootstrap 3 pages with this new library.
Tempus Dominus emerged from the eonasdan-bootstrap-datetimepicker,
which is still referenced in Bootstrap 3 pages as the datetimepicker jQuery plugin. Any references to
datetimepicker should be replaced with tempusDominus when migrating a page from
Bootstrap 3 to 5.
There are also references to the jQuery UI datepicker plugin, the timepicker plugin
(from the abandoned bootstrap-timepicker project), and daterangepicker (from
bootstrap-daterangepicker) in Bootstrap 3 pages. All of these should be replaced with Tempus Dominus
during the migration of a page from Bootstrap 3 to 5.
TempusDominus from
@eonasdan/tempus-dominus in javascript, as in the examples on this page.
Simple Date Picker Widget
If you are in need of a simple date picker widget that returns a format in YYYY-MM-DD, then
your best option is to add the date-picker CSS class to any text input and
make sure that hqwebapp/js/bootstrap5/widgets is included as part of your javascript dependencies.
The hqwebapp/js/bootstrap5/widgets module when loaded to a page will look for input fields
with the date-picker CSS class, and apply the Tempus Dominus plugin to that field.
<input class="date-picker form-control" type="text" name="a_date" />
Advanced Date Picker
If you are interested in having a date-only picker widget with extra options, you can use Tempus Dominus without the clock component. See the project's documentation for additional guidance.
<input id="js-dateonly" class="form-control" type="text" name="dateonly" />
import { TempusDominus } from '@eonasdan/tempus-dominus';
new TempusDominus(
document.getElementById('js-dateonly'),
{
display: {
theme: 'light',
components: {
clock: false,
},
},
localization: {
format: 'L',
},
},
);
Date & Time
If you want to choose a date and time together, the default state of the Tempus Dominus plugin is a good starting point. See the project's documentation for additional guidance.
<input id="js-id-date-end" class="form-control" type="text" name="date_end" />
import { TempusDominus } from '@eonasdan/tempus-dominus';
new TempusDominus(
document.getElementById('js-id-date-end'),
{
display: {
theme: 'light',
},
},
);
Time Only
If you only need to choose a time, you can use Tempus Dominus without the calendar option. See the project's documentation for additional guidance.
bootstrap-timepicker plugin. The example below should be a good starting point to use as a drop-in
replacement for that widget.
<input id="js-id-timepicker" class="form-control" type="text" name="timepick" />
import { TempusDominus } from '@eonasdan/tempus-dominus';
new TempusDominus(
document.getElementById('js-id-timepicker'),
{
display: {
theme: 'light',
components: {
calendar: false,
},
},
localization: {
format: 'LT',
},
},
);
If you want a time-picker in 24-hour format, the example below provides a good starting point.
<input id="js-id-timepicker-24" class="form-control" type="text" name="timepick24" />
import { TempusDominus } from '@eonasdan/tempus-dominus';
new TempusDominus(
document.getElementById('js-id-timepicker-24'),
{
display: {
theme: 'light',
components: {
calendar: false,
},
},
localization: {
hourCycle: 'h23',
format: 'H:mm',
},
},
);
Date Range
If you need to select a range between two dates (start and end), you can use Tempus Dominus with the
dateRange option enabled. Previously, we used bootstrap-daterangepicker as
the plugin of choice. The example below provides a drop-in replacement for the default usage of
bootstrap-daterangepicker if you come across it in older Bootstrap 3 pages.
For additional guidance, see the documentation
for Tempus Dominus.
<input id="js-date-range" class="form-control" type="text" name="date_range" />
import { TempusDominus } from '@eonasdan/tempus-dominus';
new TempusDominus(
document.getElementById('js-date-range'),
{
dateRange: true,
multipleDatesSeparator: " - ",
display: {
theme: 'light',
components: {
clock: false,
},
},
localization: {
format: 'L',
},
},
);
Using the x-datepicker Alpine directive
In addition to CSS-class-based widgets, we provide an Alpine directive,
x-datepicker, that wraps Tempus Dominus with sensible defaults. This is useful
when you are already using Alpine for other page behavior.
Important: to use x-datepicker on a page, make sure the
JavaScript entry file for that page imports the shared directive:
import "hqwebapp/js/alpinejs/directives/datepicker";
The directive is defined here.
It supports a small JSON configuration object passed via the x-datepicker /
x_datepicker attribute:
-
datetime: true— enable date and time selection, using a 24-hour clock and the formatyyyy-MM-dd H:mm:ss. -
useInputGroup: true— attach the picker to the surrounding.input-group(for use with crispyAppendedText/PrependedText). The input must be inside an.input-group. -
container: "#selector"— render the popup inside a specific DOM container (for example, an offcanvas or modal).
Simple inline usage (date only)
For a simple date-only picker rendered directly on the page, you can use
x-datepicker without any extra configuration:
Selected date:
<div
class="card mb-3"
x-data="{
selectedDate: '',
}"
>
<div class="card-body">
<label
for="alpine-datepicker-simple"
class="form-label"
>
Simple date picker (YYYY-MM-DD)
</label>
<input
id="alpine-datepicker-simple"
type="text"
class="form-control w-auto"
x-model="selectedDate"
x-datepicker=""
placeholder="YYYY-MM-DD"
autocomplete="off"
/>
<p class="mt-2 mb-0 small text-muted">
<strong>Selected date:</strong>
<span x-text="selectedDate || '—'"></span>
</p>
</div>
</div>
Date & time with x-datepicker
To capture both date and time (24-hour clock), pass datetime: true in the
configuration JSON:
Selected date & time:
<div
class="card mb-3"
x-data="{
selectedDateTime: '',
}"
>
<div class="card-body">
<label
for="alpine-datepicker-datetime"
class="form-label"
>
Date & time picker (24-hour, yyyy-MM-dd H:mm:ss)
</label>
<input
id="alpine-datepicker-datetime"
type="text"
class="form-control w-auto"
x-model="selectedDateTime"
x-datepicker='{"datetime": true}'
placeholder="YYYY-MM-DD HH:MM:SS"
autocomplete="off"
/>
<p class="mt-2 mb-0 small text-muted">
<strong>Selected date & time:</strong>
<span x-text="selectedDateTime || '—'"></span>
</p>
</div>
</div>
Using x_datepicker in crispy forms
When working with crispy forms, you typically pass the configuration as JSON into the
x_datepicker attribute on a crispy.Field. The directive will
automatically handle the Tempus Dominus initialization and cleanup.
Combine this pattern with AppendedText to add an icon, as shown in the example.
import json
from crispy_forms import bootstrap as twbscrispy
from crispy_forms import layout as crispy
from django import forms
from django.utils.safestring import mark_safe
from corehq.apps.hqwebapp import crispy as hqcrispy
class DatepickerAlpineForm(forms.Form):
datepicker = forms.CharField(
label="Date",
required=False
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = hqcrispy.HQFormHelper()
self.helper.form_method = 'POST'
self.helper.form_action = '#'
alpine_data_model = {
'datepicker': self.fields['datepicker'].initial,
}
# Layout: Alpine is used to toggle visibility of the "value" field
# based on the selected match type.
self.helper.layout = crispy.Layout(
crispy.Div(
twbscrispy.AppendedText(
'datepicker',
mark_safe( # nosec: no user input
'<i class="fcc fcc-fd-datetime"></i>'
),
x_datepicker=json.dumps(
{
'datetime': True,
'useInputGroup': True,
}
),
),
# Bind the Alpine data model defined above to this wrapper <div>.
x_data=json.dumps(alpine_data_model),
),
)
Behavior details:
- For date-only pickers, selecting a date is a single-click action; the directive automatically hides the picker after a successful selection.
-
For date+time pickers (
datetime: true), the widget shows a close button and does not auto-hide, since picking both date and time is usually a multi-step interaction. - If the input value cannot be parsed, the directive clears the Tempus Dominus value and stops the error from bubbling further, preventing the widget from getting into a broken state.