Bootstrap 6 Date Range Picker

Date Range Picker

CoreUI PRO
This component is part of CoreUI PRO – a powerful UI library with over 250 components and 25+ templates, designed to help you build modern, responsive apps faster. Fully compatible with Angular, Bootstrap, React.js, and Vue.js.

Create a start and end date field with a multi-month calendar popup. Users type both dates into section-based masks or pick the range from the calendar, with optional predefined ranges.

Example

Days

Two masked fields share one input-group frame; the calendar spans two months and owns the range mechanics — pick a start day and the selection advances to the end date automatically. Focusing the end field switches the calendar to end-date selection, the way clicking the end input does in v1.

html
<div class="row">
  <div class="col-lg-7 mb-4">
    <div data-coreui-locale="en-US" data-coreui-date-range-picker></div>
  </div>
</div>
<div class="row">
  <div class="col-lg-7">
    <div
      data-coreui-end-date="2026/03/20"
      data-coreui-locale="en-US"
      data-coreui-start-date="2026/03/15"
      data-coreui-date-range-picker></div>
  </div>
</div>

There is no footer option — project a <template data-coreui-template="footer"> child. With a footer present, completing a range does not auto-close; closing is the footer’s job.

html
<div class="row">
  <div class="col-lg-7">
    <div data-coreui-locale="en-US" data-coreui-date-range-picker>
      <template data-coreui-template="footer">
        <button type="button" class="btn-ghost theme-primary btn-sm" data-coreui-picker-action="clear">Clear</button>
        <button type="button" class="btn-solid theme-primary btn-sm" data-coreui-picker-action="close">OK</button>
      </template>
    </div>
  </div>
</div>

Weeks

Add data-coreui-selection-type="week" to select week ranges, and data-coreui-show-week-number="true" to show week numbers. The fields render the native <input type="week"> presentation (Week 29, 2026, with a localized fixed label) — see the date picker note on the ISO week-numbering year.

html
<div class="row">
  <div class="col-lg-7">
    <div
      data-coreui-locale="en-US"
      data-coreui-selection-type="week"
      data-coreui-show-week-number="true"
      data-coreui-date-range-picker></div>
  </div>
</div>

Months

html
<div class="row">
  <div class="col-lg-7">
    <div
      data-coreui-locale="en-US"
      data-coreui-selection-type="month"
      data-coreui-date-range-picker></div>
  </div>
</div>

Quarters

The field masks narrow to quarter and year sections (Q1 2026).

html
<div class="row">
  <div class="col-lg-7">
    <div
      data-coreui-locale="en-US"
      data-coreui-selection-type="quarter"
      data-coreui-date-range-picker></div>
  </div>
</div>

Years

html
<div class="row">
  <div class="col-lg-7">
    <div
      data-coreui-locale="en-US"
      data-coreui-selection-type="year"
      data-coreui-date-range-picker></div>
  </div>
</div>

With form field

Wrap the control in .form-field to lay it out with its label and description, and to get validation feedback in the right place.

Reporting period
Both dates are included in the report.
html
<div class="form-field">
  <span class="form-label" id="dateRangePickerReportingPeriodLabel">Reporting period</span>
  <div data-coreui-locale="en-US" data-coreui-date-range-picker aria-labelledby="dateRangePickerReportingPeriodLabel"></div>
  <div class="form-text">Both dates are included in the report.</div>
</div>

Floating labels

Each field carries its own floating label: set startFloatingLabel and endFloatingLabel and the picker renders a label over each date, floating independently — fill in the check-in date and only its label rises. The label text also becomes the field’s accessible name, so the visible and spoken labels stay one thing. No wrapper markup is needed.

html
<div data-coreui-locale="en-US" data-coreui-start-floating-label="Check-in" data-coreui-end-floating-label="Check-out" data-coreui-date-range-picker></div>

Add .form-floating-always to the component element to keep both labels always floating with the masks showing.

Sizing

html
<div class="row mb-4">
  <div class="col-lg-7">
    <div data-coreui-locale="en-US" data-coreui-size="lg" data-coreui-date-range-picker></div>
  </div>
</div>
<div class="row">
  <div class="col-lg-7">
    <div data-coreui-locale="en-US" data-coreui-size="sm" data-coreui-date-range-picker></div>
  </div>
</div>

Disabled

html
<div class="row">
  <div class="col-lg-7">
    <div data-coreui-disabled="true" data-coreui-locale="en-US" data-coreui-date-range-picker></div>
  </div>
</div>

Disabled dates

html
<div class="row">
  <div class="col-lg-7">
    <div id="myDateRangePickerDisabledDates"></div>
  </div>
</div>
js
const myDateRangePickerDisabledDates = document.getElementById('myDateRangePickerDisabledDates')

const dateRangePickerDisabledDates = new coreui.DateRangePicker(myDateRangePickerDisabledDates, {
  locale: 'en-US',
  startDate: new Date(2026, 6, 10),
  endDate: new Date(2026, 6, 14),
  disabledDates: [
    [new Date(2026, 6, 4), new Date(2026, 6, 7)],
    new Date(2026, 6, 16)
  ],
  minDate: new Date(2026, 6, 1),
  maxDate: new Date(2026, 7, 31)
})

myDateRangePickerDisabledDates.addEventListener('endDateChange.coreui.date-range-picker', event => {
  console.log('endDateChange', event.date, event.formattedDate)
})

Validation

The picker takes its validation state the way the date range input does: an end before the start makes the frame invalid while it stands and keeps both fields from being drawn valid, and a state you give lands on the frame and on both ends. You give it with the validationState option ('valid', 'invalid' or a state of $form-validation-states, with invalid and valid as deprecated aliases) or with an .is-invalid or .is-valid class written on the picker element, the server-side route, which is read once, when the picker starts; the option wins over the class. While the given state is 'invalid', every section of both fields carries aria-invalid="true" and is described by the message after the picker, and both ends block the submit with its text, or Invalid value. when there is none.

Those dates are already booked.
html
<div class="row">
  <div class="col-lg-7 form-field">
    <div class="is-invalid" data-coreui-date-range-picker data-coreui-locale="en-US" data-coreui-start-date="2026-07-14" data-coreui-end-date="2026-07-20"></div>
    <div class="invalid-feedback">Those dates are already booked.</div>
  </div>
</div>

A given state is a verdict on the range it was given for, so the next change of the range the user makes drops it from both ends and from the picker element: a date picked in the calendar, the cleaner, a button in the footer or among the custom ranges that changes the range through the context, or typing in either field. An action that leaves the range as it is keeps the state. A change your own code makes, with setRange(), clear() or reset(), keeps it. setValidationState() sets it again without rebuilding the picker. A class or an aria-invalid written on the picker element after it starts is not read. A native form reset puts back the range the picker started with and drops the state.

Blocking the submit

A required end that is empty, incomplete or rejected by the limits blocks the submit by itself. An end before the start does not, since both ends hold dates the browser accepts, and neither does an optional end that is empty, incomplete or rejected, which submits no date. Check the range in your submit listener; getStartDate() and getEndDate() return null for an end that is empty, incomplete or rejected, so one condition covers both:

const element = document.querySelector('#stayRange')
const range = coreui.DateRangePicker.getOrCreateInstance(element)

element.closest('form').addEventListener('submit', event => {
  const start = range.getStartDate()
  const end = range.getEndDate()

  if (!start || !end || end < start) {
    event.preventDefault()
  }
})

Custom ranges

There is no ranges option. Project a <template data-coreui-template="ranges"> child — it renders as a column beside the calendars. Presets take arguments, so they run through getContext().setRange(): the date math, labels, and order are yours.

html
<div class="row">
  <div class="col-lg-8">
    <div id="myDateRangePickerRanges">
      <template data-coreui-template="ranges">
        <button type="button" class="btn-ghost theme-primary btn-sm" id="rangeLast7">Last 7 days</button>
        <button type="button" class="btn-ghost theme-primary btn-sm" id="rangeLast30">Last 30 days</button>
        <button type="button" class="btn-ghost theme-primary btn-sm" id="rangeThisMonth">This month</button>
      </template>
      <template data-coreui-template="footer">
        <button type="button" class="btn-ghost theme-primary btn-sm" data-coreui-picker-action="clear">Clear</button>
        <button type="button" class="btn-solid theme-primary btn-sm" data-coreui-picker-action="close">OK</button>
      </template>
    </div>
  </div>
</div>
js
const myDateRangePickerRanges = document.getElementById('myDateRangePickerRanges')

const dateRangePickerRanges = new coreui.DateRangePicker(myDateRangePickerRanges, {
  locale: 'en-US',
  startName: 'report-start',
  endName: 'report-end'
})

// Projected ranges act through the slot context — labels, order, and the
// date math are userland, the shell only exposes setRange.
const context = () => dateRangePickerRanges.getContext()
const today = new Date()

const presets = {
  rangeLast7: () => context().setRange(new Date(today.getFullYear(), today.getMonth(), today.getDate() - 6), today),
  rangeLast30: () => context().setRange(new Date(today.getFullYear(), today.getMonth(), today.getDate() - 29), today),
  rangeThisMonth: () => context().setRange(new Date(today.getFullYear(), today.getMonth(), 1), new Date(today.getFullYear(), today.getMonth() + 1, 0))
}

for (const [id, apply] of Object.entries(presets)) {
  myDateRangePickerRanges.querySelector(`#${id}`).addEventListener('click', apply)
}

Locale

Auto

html
<div class="row">
  <div class="col-lg-7">
    <div data-coreui-date-range-picker></div>
  </div>
</div>

Chinese

html
<div class="row">
  <div class="col-lg-7">
    <div data-coreui-locale="zh-CN" data-coreui-date-range-picker></div>
  </div>
</div>

Japanese

html
<div class="row">
  <div class="col-lg-7">
    <div data-coreui-locale="ja" data-coreui-date-range-picker></div>
  </div>
</div>

Korean

html
<div class="row">
  <div class="col-lg-7">
    <div data-coreui-locale="ko" data-coreui-date-range-picker></div>
  </div>
</div>

Right to left support

Hebrew

html
<div class="row" dir="rtl">
  <div class="col-lg-7">
    <div data-coreui-locale="he-IL" data-coreui-date-range-picker></div>
  </div>
</div>

Persian

The date range picker is Gregorian: with a locale whose own calendar has different months, like Persian, its calendar shows the Gregorian months in that locale’s language and digits, and its fields hold Gregorian numbers.

html
<div class="row" dir="rtl">
  <div class="col-lg-7">
    <div data-coreui-locale="fa-IR" data-coreui-date-range-picker></div>
  </div>
</div>

The separator between the two fields is a directional arrow, so the component renders its mirrored counterpart (separatorIconRtl) whenever the picker sits in an RTL context — the document direction or any dir="rtl" ancestor.

Custom formats

The mask of the fields defines the format. Set it with data-coreui-format, forwarded to both fields.

html
<div class="row">
  <div class="col-lg-7">
    <div data-coreui-format="dd.MM.yyyy" data-coreui-locale="en-US" data-coreui-date-range-picker></div>
  </div>
</div>

Accessibility

The fields and the popup answer to the same keys as the date picker: Alt (Option on macOS) + Arrow Down or F4 opens the calendars, Escape closes them and returns focus, and arrows, Home / End, and Page Down / Page Up (with Shift for years) move the focused date.

The picker element is a role="group", so an aria-labelledby on it, pointing at the visible label as the example on this page does, names the pair of fields, and an aria-describedby on it moves to both fields. Clicking that label, or a <label for> pointing at the element, moves focus to the first empty section of the start date.

The toggle’s name ends with the range the picker holds, written by the locale (Toggle calendar, Thursday, August 20 – Sunday, August 23, 2026), so a screen reader reads the picked range when the popup closes and focus goes back to the toggle. A live message would be lost at that moment, so the calendar does not announce the pick of the end that closes the popup. A pick that leaves the popup open, such as a new start that keeps the end or any pick with a footer, is announced through ariaSelectedRangeAnnouncement, and so is every pick without a toggle (pickerIcon: false).

Usage

Via data attributes

Add data-coreui-date-range-picker to a div element.

<div data-coreui-date-range-picker></div>

Via JavaScript

<div class="my-date-range-picker"></div>
const pickerElementList = Array.prototype.slice.call(document.querySelectorAll('.my-date-range-picker'))
const pickerList = pickerElementList.map(pickerEl => {
  return new coreui.DateRangePicker(pickerEl)
})

Options

Options belonging to the inner calendar or date range input are forwarded by name, so their data attributes work on the picker element too. The calendar’s range and selectEndDate are set by the picker itself.

PropertyTypeDefault
allowListobjectSVGAllowlist

Allowlist used when sanitizing icon markup.

ariaCleanerLabelstring'Clear date range'

Accessible label of the cleaner button.

ariaEndLabelstring'End date'

Accessible name of the end field when it has no floating label. Both halves are the same control, so naming them apart is what tells a screen reader which end of the range it is on.

ariaPickerLabelstring'Toggle calendar'

Accessible label of the picker button, followed by the range the picker holds.

ariaPopupLabelstring'Calendar'

Accessible name of the calendar panel, which opens as a role="dialog".

ariaStartLabelstring'Start date'

Accessible name of the start field when it has no floating label.

calendarOptionsobject{}

Programmatic escape hatch for calendar options.

calendarsnumber2

Months shown side by side.

cleanerbooleantrue

Renders a button that clears the value. It appears only once the field holds one.

cleanerIconstringcross SVG

Inline SVG rendered by the component (sanitized).

containerstring, element, falsefalse

Teleport target for the dropdown. Inside a modal dialog it goes into the dialog instead, because the rest of the page is inert.

disabledbooleanfalse

Disables the fields and the popup.

endDatedate, string, nullnull

Initial end of the range.

endFloatingLabelstring, nullnull

Renders a floating label over the end field, which also becomes that field’s accessible name.

endNamestring, nullnull

Name of the end value input. Without it the end date is not submitted with the form.

formatfunction, string, nullnull

Mask layout of both fields: null derives it from the locale (or, for week, month, quarter and year selection, uses the matching default mask), a string is parsed as format tokens, a function receives the locale and returns the section layout.

inputOptionsobject{}

Programmatic escape hatch for date input options, except validationState, which the picker owns.

invalidbooleanfalse

Deprecated alias of validationState: 'invalid', ignored once validationState is set.

localestringbrowser locale

Drives section order, separators, and calendar labels.

maxDatedate, string, nullnull

Upper bound, enforced by both layers.

minDatedate, string, nullnull

Lower bound, enforced by both layers.

pickerIconstring, booleantrue

The button that opens the calendar: true for the built-in icon, an inline SVG string for your own (sanitized), false for no button. Without it the popup has no pointer opener and nothing reports the expanded state, though the field keeps the keyboard openers below.

sanitizebooleantrue

Sanitize the icon markup.

sanitizeFnfunction, nullnull

Custom sanitizer.

selectionTypestring'day'

What the picker selects: 'day', 'week', 'month', 'quarter' or 'year'. The calendar and the field masks follow it.

separatorIconstringarrow SVG

Inline SVG between the two fields (LTR).

separatorIconRtlstringmirrored arrow SVG

Inline SVG used when the picker renders in an RTL context.

sizestring, nullnull

'sm' or 'lg'.

startDatedate, string, nullnull

Initial start of the range.

startFloatingLabelstring, nullnull

Renders a floating label over the start field, which also becomes that field’s accessible name.

startNamestring, nullnull

Name of the start value input. Without it the start date is not submitted with the form.

validbooleanfalse

Deprecated alias of validationState: 'valid', ignored once validationState is set.

validationStatestring, nullnull

The validation state of the range, shown on the frame and on both ends: 'valid', 'invalid' or a state of $form-validation-states. 'invalid' also blocks the submit. The next change the user makes to the range clears it, see Validation.

Methods

MethodDescription
clearClears the range.
disposeDestroys the picker and its inner components.
getContextReturns the slot context (see below).
getEndDateReturns the end of the range (Date or null).
getInstanceStatic — returns the instance for a DOM element.
getOrCreateInstanceStatic — returns the instance, creating it if needed.
getStartDateReturns the start of the range (Date or null).
hideCloses the popup.
resetRestores the initial range.
setEndDate(date)Sets the end date and keeps the start, without an event.
setValidationState(state)Gives the picker a validation state ('valid', 'invalid' or a state added to $form-validation-states), or drops it with null. The value stays. See Validation.
setStartDate(date)Sets the start date and keeps the end, without an event.
setRangeSets both ends without an event. The open calendar and the name of the toggle follow it.
showOpens the popup.
toggleToggles the popup.

Slot context

const picker = coreui.DateRangePicker.getOrCreateInstance(element)
const context = picker.getContext()

context.setRange(start, end)   // set both ends (null allowed)
context.clear()                // clear the range
context.reset()                // restore the initial range
context.close()                // close the popup
context.startDate              // current start (Date | null)
context.endDate                // current end (Date | null)
context.disabled               // disabled state

Events

EventDescription
endDateChange.coreui.date-range-pickerFired when the end changes, with the same payload shape.
hidden.coreui.date-range-pickerFired once the popup has finished its exit transition and left the page. Skipped when the popup reopens first.
hide.coreui.date-range-pickerFired immediately when hide is called.
show.coreui.date-range-pickerFired immediately when show is called. Call event.preventDefault() to keep the popup closed.
shown.coreui.date-range-pickerFired once the popup has finished its entry transition. Skipped when the popup closes first.
startDateChange.coreui.date-range-pickerFired when the start changes. event.date is the validated Date, or null; event.formattedDate is the same value in the shape of selectionType (2026-01, 2026W03, 2026Q1, 2026, or the Date itself for day), with the year in at least four digits (0026-01).

startDateChange and endDateChange fire before the fields’ errorChange.coreui.date-input for the same change, whether it comes from the calendar, the fields or an action such as clear() or getContext().setRange(); the setters do not fire them.

const myPicker = document.getElementById('myPicker')
myPicker.addEventListener('endDateChange.coreui.date-range-picker', event => {
  console.log(event.date, event.formattedDate)
})

Customizing

CSS variables

The field is a .form-control-group, .form-date-range, inside the picker element, so its frame, the separator, the cleaner and the toggle read the --cui-control-* and --cui-control-group-* tokens. Set them on .form-date-range; the group declares them itself, so a value set on the picker element stops there. The two ends are date inputs and carry the form-date-time tokens.

The panel is the date picker’s, and declares its own on .date-picker-popup:

scss
$date-picker-tokens: defaults(
  (
    --cui-date-picker-ranges-padding-y: .5rem,
    --cui-date-picker-ranges-padding-x: .5rem,
    --cui-date-picker-ranges-gap: .25rem,
    --cui-date-picker-ranges-border-width: var(--cui-border-width),
    --cui-date-picker-ranges-border-color: var(--cui-border-color),
    --cui-date-picker-timepickers-border-width: var(--cui-border-width),
    --cui-date-picker-timepickers-border-color: var(--cui-border-color),
    --cui-date-picker-footer-padding-y: .5rem,
    --cui-date-picker-footer-padding-x: .5rem,
    --cui-date-picker-footer-gap: .5rem,
    --cui-date-picker-footer-border-width: var(--cui-border-width),
    --cui-date-picker-footer-border-color: var(--cui-border-color),
  ),
  $date-picker-tokens
);

The panel is a .popup: its surface, border, shadow, stacking and fade come from the tokens every popup panel shares with the other pickers, Combobox, Autocomplete and Multi Select. .popup declares them itself, so set them on .date-picker-popup to restyle the panels of the date and date range pickers, or on .popup for all of them; a value set on an ancestor or on :root does not reach it.

scss
$popup-tokens: () !default;
$popup-tokens: defaults(
  (
    --cui-popup-zindex: var(--cui-z-dropdown),
    --cui-popup-bg: var(--cui-bg-body),
    --cui-popup-border-width: var(--cui-border-width),
    --cui-popup-border-color: var(--cui-border-color),
    --cui-popup-border-radius: var(--cui-radius-7),
    --cui-popup-box-shadow: var(--cui-box-shadow),
    --cui-popup-transition-duration: .15s,
    --cui-popup-transition-timing: var(--cui-transition-timing-overlay),
  ),
  $popup-tokens
);
scss
$calendar-tokens: defaults(
  (
    --cui-calendar-table-margin: .5rem,
    --cui-calendar-table-cell-size: 2.75rem,
    --cui-calendar-nav-padding-y: .5rem,
    --cui-calendar-nav-padding-x: .5rem,
    --cui-calendar-nav-border-color: var(--cui-border-color),
    --cui-calendar-nav-border: var(--cui-border-width) solid var(--cui-calendar-nav-border-color),
    --cui-calendar-nav-btn-padding-x: .5rem,
    --cui-calendar-nav-btn-padding-y: .25rem,
    --cui-calendar-nav-btn-font-size: var(--cui-font-size-sm),
    --cui-calendar-nav-btn-bg: transparent,
    --cui-calendar-nav-btn-border-width: 1px,
    --cui-calendar-nav-btn-border-color: transparent,
    --cui-calendar-nav-btn-border-radius: var(--cui-radius-4),
    --cui-calendar-nav-btn-hover-bg: transparent,
    --cui-calendar-nav-btn-hover-border-color: transparent,
    --cui-calendar-nav-btn-focus-border-color: transparent,
    --cui-calendar-nav-date-font-weight: 600,
    --cui-calendar-nav-date-color: var(--cui-fg-body),
    --cui-calendar-nav-date-hover-color: var(--cui-primary-base),
    --cui-calendar-nav-icon-width: 1rem,
    --cui-calendar-nav-icon-height: 1rem,
    --cui-calendar-nav-icon-color: var(--cui-fg-3),
    --cui-calendar-nav-icon-hover-color: var(--cui-fg-body),
    --cui-calendar-cell-header-inner-font-weight: 600,
    --cui-calendar-cell-header-inner-color: var(--cui-fg-3),
    --cui-calendar-cell-week-number-font-weight: 600,
    --cui-calendar-cell-week-number-color: var(--cui-fg-2),
    --cui-calendar-cell-border-radius: var(--cui-radius-4),
    --cui-calendar-cell-hover-color: var(--cui-fg-body),
    --cui-calendar-cell-hover-bg: var(--cui-bg-2),
    --cui-calendar-cell-disabled-color: var(--cui-fg-4),
    --cui-calendar-cell-selected-color: var(--cui-primary-contrast),
    --cui-calendar-cell-selected-bg: var(--cui-primary-base),
    --cui-calendar-cell-selected-hover-color: var(--cui-calendar-cell-selected-color),
    --cui-calendar-cell-selected-hover-bg: oklch(from var(--cui-calendar-cell-selected-bg) calc(l * var(--cui-theme-hover-lightness)) calc(c * var(--cui-theme-hover-chroma)) h),
    --cui-calendar-cell-selected-adjacent-color: var(--cui-primary-fg-emphasis),
    --cui-calendar-cell-selected-adjacent-bg: var(--cui-primary-bg-subtle),
    --cui-calendar-cell-selected-adjacent-hover-color: var(--cui-calendar-cell-selected-adjacent-color),
    --cui-calendar-cell-selected-adjacent-hover-bg: var(--cui-primary-bg-muted),
    --cui-calendar-cell-range-bg: var(--cui-primary-bg-subtle),
    --cui-calendar-cell-range-hover-border-color: var(--cui-primary-base),
    --cui-calendar-cell-today-color: var(--cui-danger-base),
  ),
  $calendar-tokens
);