Skip to component content

Select single dates and ranges with a button trigger, popover, and keyboardable calendar.

Usage

Use [data-datepicker] for a single date, [data-datepicker-range] for start/end ranges, and [data-calendar] when the calendar should stay inline. The trigger owns aria-expanded and aria-controls; the popover owns role="dialog"; the calendar owns roving day focus and selection state.

The optional Datepicker ESM is a self-contained Moo UI module with no third-party runtime; it owns the visible surface, date math, and selection state. Import @wpmoo/ui/datepicker.js, initialize only the root your page owns, and call dispose() before removing that root in a partial-page or SPA update.

Basic

A button-like trigger opens a compact calendar popover and writes the selected value to a hidden form field.

Date Picker
<div class="field w-100">
  <div class="moo-datepicker" id="datepicker-basic" data-datepicker data-datepicker-mode="single" data-datepicker-locale="en" data-datepicker-placeholder="Pick a date">
    <label class="form-label" for="datepicker-basic-trigger">Date</label>
    <button class="btn btn-outline-secondary moo-datepicker__trigger" id="datepicker-basic-trigger" type="button" data-datepicker-trigger aria-haspopup="dialog" aria-expanded="false" aria-controls="datepicker-basic-popover">
      <i class="lucide lucide-calendar" data-icon="inline-start" aria-hidden="true"></i>
      <span data-datepicker-label>Pick a date</span>
    </button>
    <input type="hidden" name="date" value="" data-datepicker-input>
    <div class="moo-datepicker__popover" id="datepicker-basic-popover" data-datepicker-popover role="dialog" aria-modal="false" aria-label="Date calendar" hidden>
      <div class="moo-calendar" id="datepicker-basic-calendar" data-calendar data-calendar-mode="single" data-calendar-locale="en" aria-label="Date calendar" tabindex="-1"></div>
    </div>
  </div>
</div>

Time Picker

A Date Picker paired with a native time input for selecting a time; the time field stays a plain form control.

Date Picker
<div class="moo-datepicker-time-row">
  <div class="field flex-grow-1">
    <div class="moo-datepicker" id="datepicker-time-picker" data-datepicker data-datepicker-mode="single" data-datepicker-locale="en" data-datepicker-placeholder="Pick a date">
      <label class="form-label" for="datepicker-time-picker-trigger">Date</label>
      <button class="btn btn-outline-secondary moo-datepicker__trigger" id="datepicker-time-picker-trigger" type="button" data-datepicker-trigger aria-haspopup="dialog" aria-expanded="false" aria-controls="datepicker-time-picker-popover">
        <i class="lucide lucide-calendar" data-icon="inline-start" aria-hidden="true"></i>
        <span data-datepicker-label>Pick a date</span>
      </button>
      <input type="hidden" name="event_date" value="" data-datepicker-input>
      <div class="moo-datepicker__popover" id="datepicker-time-picker-popover" data-datepicker-popover role="dialog" aria-modal="false" aria-label="Date calendar" hidden>
        <div class="moo-calendar" id="datepicker-time-picker-calendar" data-calendar data-calendar-mode="single" data-calendar-locale="en" aria-label="Date calendar" tabindex="-1"></div>
      </div>
    </div>
  </div>
  <div class="field moo-datepicker-time">
    <label class="form-label" for="datepicker-time-picker-time">Time</label>
    <input type="time" class="form-control" id="datepicker-time-picker-time" name="event_time" value="10:30:00" step="1" autocomplete="off">
  </div>
</div>

Range Picker

One trigger manages two hidden fields; selecting the end date completes the range and closes the picker with focus returning to the trigger.

Date Picker
<div class="field w-100">
  <div class="moo-datepicker moo-datepicker--range" id="datepicker-range" data-datepicker-range data-datepicker-mode="range" data-datepicker-locale="en" data-datepicker-placeholder="Pick a date range" data-datepicker-start-value="2026-08-20" data-datepicker-end-value="2026-08-27">
    <label class="form-label" for="datepicker-range-trigger">Date range</label>
    <button class="btn btn-outline-secondary moo-datepicker__trigger" id="datepicker-range-trigger" type="button" data-datepicker-trigger aria-haspopup="dialog" aria-expanded="false" aria-controls="datepicker-range-popover">
      <i class="lucide lucide-calendar-range" data-icon="inline-start" aria-hidden="true"></i>
      <span data-datepicker-label>Aug 20, 2026 - Aug 27, 2026</span>
    </button>
    <input type="hidden" name="start_date" value="2026-08-20" data-datepicker-range-start>
    <input type="hidden" name="end_date" value="2026-08-27" data-datepicker-range-end>
    <div class="moo-datepicker__popover" id="datepicker-range-popover" data-datepicker-popover role="dialog" aria-modal="false" aria-label="Date range calendar" hidden>
      <div class="moo-calendar" id="datepicker-range-calendar" data-calendar data-calendar-mode="range" data-calendar-locale="en" data-calendar-start-value="2026-08-20" data-calendar-end-value="2026-08-27" aria-label="Date range calendar" tabindex="-1"></div>
    </div>
  </div>
</div>

Date Of Birth

Month and year selectors keep long historical ranges manageable without changing the calendar contract.

Date Picker
<div class="field w-100">
  <div class="moo-datepicker" id="datepicker-birth-date" data-datepicker data-datepicker-mode="single" data-datepicker-locale="en" data-datepicker-placeholder="Select date" data-datepicker-min-date="1926-01-01" data-datepicker-max-date="2026-12-31">
    <label class="form-label" for="datepicker-birth-date-trigger">Date of birth</label>
    <button class="btn btn-outline-secondary moo-datepicker__trigger" id="datepicker-birth-date-trigger" type="button" data-datepicker-trigger aria-haspopup="dialog" aria-expanded="false" aria-controls="datepicker-birth-date-popover">
      <i class="lucide lucide-calendar" data-icon="inline-start" aria-hidden="true"></i>
      <span data-datepicker-label>Select date</span>
    </button>
    <input type="hidden" name="birth_date" value="" data-datepicker-input>
    <div class="moo-datepicker__popover" id="datepicker-birth-date-popover" data-datepicker-popover role="dialog" aria-modal="false" aria-label="Date of birth calendar" hidden>
      <div class="moo-calendar" id="datepicker-birth-date-calendar" data-calendar data-calendar-mode="single" data-calendar-locale="en" data-calendar-min-date="1926-01-01" data-calendar-max-date="2026-12-31" data-calendar-caption-layout="dropdown" aria-label="Date of birth calendar" tabindex="-1"></div>
    </div>
  </div>
</div>

Presets

Preset buttons set common dates while preserving the same hidden input serialization.

Date Picker
<div class="field w-100">
  <div class="moo-datepicker" id="datepicker-presets" data-datepicker data-datepicker-mode="single" data-datepicker-locale="en" data-datepicker-placeholder="Pick a date" data-datepicker-show-presets="true">
    <label class="form-label" for="datepicker-presets-trigger">Schedule date</label>
    <button class="btn btn-outline-secondary moo-datepicker__trigger" id="datepicker-presets-trigger" type="button" data-datepicker-trigger aria-haspopup="dialog" aria-expanded="false" aria-controls="datepicker-presets-popover">
      <i class="lucide lucide-calendar" data-icon="inline-start" aria-hidden="true"></i>
      <span data-datepicker-label>Pick a date</span>
    </button>
    <input type="hidden" name="schedule_date" value="" data-datepicker-input>
    <div class="moo-datepicker__popover" id="datepicker-presets-popover" data-datepicker-popover role="dialog" aria-modal="false" aria-label="Schedule date calendar" hidden>
      <div class="moo-calendar" id="datepicker-presets-calendar" data-calendar data-calendar-mode="single" data-calendar-locale="en" data-calendar-show-presets="true" aria-label="Schedule date calendar" tabindex="-1"></div>
    </div>
  </div>
</div>

Inline Calendar

Use the same calendar directly when the date choice is the main task on the screen.

Date Picker
<div class="moo-calendar" id="datepicker-inline-calendar" data-calendar data-calendar-mode="single" data-calendar-locale="en" data-calendar-value="2026-08-18" data-calendar-min-date="2026-08-10" data-calendar-max-date="2026-09-15" data-calendar-disabled-dates='["2026-08-22", "2026-08-23"]' data-calendar-show-presets="true" aria-label="Release calendar" tabindex="-1"></div>

RTL

The popover follows the trigger direction and the locale option controls visible formatting in the calendar.

<div class="field w-100">
  <div class="moo-datepicker" id="datepicker-rtl-arabic" data-datepicker data-datepicker-mode="single" data-datepicker-locale="ar" data-datepicker-placeholder="اختر تاريخا" data-datepicker-value="2026-08-18" dir="rtl">
    <button class="btn btn-outline-secondary moo-datepicker__trigger" id="datepicker-rtl-arabic-trigger" type="button" data-datepicker-trigger aria-haspopup="dialog" aria-expanded="false" aria-controls="datepicker-rtl-arabic-popover" aria-label="اختر تاريخا">
      <i class="lucide lucide-calendar" data-icon="inline-start" aria-hidden="true"></i>
      <span data-datepicker-label>Aug 18, 2026</span>
    </button>
    <input type="hidden" name="rtl_arabic_date" value="2026-08-18" data-datepicker-input>
    <div class="moo-datepicker__popover" id="datepicker-rtl-arabic-popover" data-datepicker-popover role="dialog" aria-modal="false" aria-label="اختر تاريخا calendar" hidden>
      <div class="moo-calendar" id="datepicker-rtl-arabic-calendar" data-calendar data-calendar-mode="single" data-calendar-locale="ar" data-calendar-value="2026-08-18" aria-label="اختر تاريخا calendar" tabindex="-1"></div>
    </div>
  </div>
</div>
<div class="field w-100">
  <div class="moo-datepicker" id="datepicker-rtl-hebrew" data-datepicker data-datepicker-mode="single" data-datepicker-locale="he" data-datepicker-placeholder="בחר תאריך" data-datepicker-value="2026-08-18" dir="rtl">
    <button class="btn btn-outline-secondary moo-datepicker__trigger" id="datepicker-rtl-hebrew-trigger" type="button" data-datepicker-trigger aria-haspopup="dialog" aria-expanded="false" aria-controls="datepicker-rtl-hebrew-popover" aria-label="בחר תאריך">
      <i class="lucide lucide-calendar" data-icon="inline-start" aria-hidden="true"></i>
      <span data-datepicker-label>Aug 18, 2026</span>
    </button>
    <input type="hidden" name="rtl_hebrew_date" value="2026-08-18" data-datepicker-input>
    <div class="moo-datepicker__popover" id="datepicker-rtl-hebrew-popover" data-datepicker-popover role="dialog" aria-modal="false" aria-label="בחר תאריך calendar" hidden>
      <div class="moo-calendar" id="datepicker-rtl-hebrew-calendar" data-calendar data-calendar-mode="single" data-calendar-locale="he" data-calendar-value="2026-08-18" aria-label="בחר תאריך calendar" tabindex="-1"></div>
    </div>
  </div>
</div>
<div class="field w-100">
  <div class="moo-datepicker" id="datepicker-rtl-english" data-datepicker data-datepicker-mode="single" data-datepicker-locale="en" data-datepicker-placeholder="Pick a date" data-datepicker-value="2026-08-18">
    <button class="btn btn-outline-secondary moo-datepicker__trigger" id="datepicker-rtl-english-trigger" type="button" data-datepicker-trigger aria-haspopup="dialog" aria-expanded="false" aria-controls="datepicker-rtl-english-popover" aria-label="Pick a date">
      <i class="lucide lucide-calendar" data-icon="inline-start" aria-hidden="true"></i>
      <span data-datepicker-label>Aug 18, 2026</span>
    </button>
    <input type="hidden" name="rtl_english_date" value="2026-08-18" data-datepicker-input>
    <div class="moo-datepicker__popover" id="datepicker-rtl-english-popover" data-datepicker-popover role="dialog" aria-modal="false" aria-label="Pick a date calendar" hidden>
      <div class="moo-calendar" id="datepicker-rtl-english-calendar" data-calendar data-calendar-mode="single" data-calendar-locale="en" data-calendar-value="2026-08-18" aria-label="Pick a date calendar" tabindex="-1"></div>
    </div>
  </div>
</div>

HTML Anatomy

Selector Purpose
Roots
[data-datepicker] Single-date root. It owns the trigger, popover, calendar root, hidden field, and instance lifecycle.
[data-datepicker-range] Range root. It uses the same popover and one calendar while syncing start and end hidden fields.
Trigger
[data-datepicker-trigger] Button-like trigger with type="button", aria-haspopup="dialog", aria-expanded, and aria-controls.
Popover
[data-datepicker-popover] Popover surface with role="dialog", aria-modal="false", and hidden while closed.
[data-calendar] Standalone calendar root. The runtime renders caption controls, weekdays, six weeks of day buttons, disabled state, selected state, and range state.

JavaScript

The module is explicit and idempotent. It does not scan the document when imported, and it owns its state internally with no global leak.

import Datepicker, {
  MooCalendar,
  MooDateRangePicker,
} from "@wpmoo/ui/datepicker.js";

const datepicker = Datepicker.getOrCreateInstance(
  document.querySelector("[data-datepicker]")
);
const range = MooDateRangePicker.getOrCreateInstance(
  document.querySelector("[data-datepicker-range]")
);
const calendar = MooCalendar.getOrCreateInstance(
  document.querySelector("#datepicker-inline-calendar")
);

datepicker.setDate("2026-08-18");
range.setDates("2026-08-20", "2026-08-27");
calendar.clear();

datepicker.dispose();
range.dispose();
calendar.dispose();

Values

Hidden form values use yyyy-mm-dd strings. Public getters return a local Date object by default, or the same yyyy-mm-dd serialization when called with that format string. The locale option controls visible formatting in the calendar and trigger label, defaulting to en.

Component reference

Status
Accepted
Runtime
optional Moo UI ESM
Markup
Moo UI documented extension
Reference

The trigger and presets are Bootstrap button primitives; the popover and calendar are Moo UI markup with natively owned date, selection, and keyboard behavior. Bootstrap ships no date picker component. For the button surface, see the Bootstrap Buttons documentation.

For the popover surface and form fields, see the Bootstrap Popovers documentation.

Settings

Theme
Choose how Moo UI looks on this device.
Sidebar
Choose how the sidebar sits on the page.
Theme Builder
Tune catalog tokens and chart colors for this browser.
Base color
Theme color
Chart color
Heading font
Body font
Radius
Direction
Choose the reading direction. RTL flips the layout live.
Reset

Restore the default appearance.