webdevpuneet.com

Booking Date Range Picker Modal — Free HTML CSS JS Snippet

Booking Date Range Picker Modal · Modals · Plain HTML, CSS & JS · Live preview

CategoryModals

Copy & paste

Source Code

<div class="drm-page"><button type="button" class="drm-open" id="drmOpen">Select dates</button></div>

<div class="drm-backdrop" id="drmBackdrop"></div>
<div class="drm-modal" id="drmModal" role="dialog" aria-modal="true" aria-labelledby="drmTitle">
  <button type="button" class="drm-close" id="drmClose" aria-label="Close">&#10005;</button>
  <h3 id="drmTitle">Select your dates</h3>

  <div class="drm-calendars">
    <div class="drm-cal">
      <div class="drm-cal-head">
        <button type="button" class="drm-nav" id="drmPrev" aria-label="Previous month">&#8249;</button>
        <span id="drmLabelLeft"></span>
        <span class="drm-nav-spacer"></span>
      </div>
      <div class="drm-weekdays"><span>S</span><span>M</span><span>T</span><span>W</span><span>T</span><span>F</span><span>S</span></div>
      <div class="drm-grid" id="drmGridLeft"></div>
    </div>
    <div class="drm-cal">
      <div class="drm-cal-head">
        <span class="drm-nav-spacer"></span>
        <span id="drmLabelRight"></span>
        <button type="button" class="drm-nav" id="drmNext" aria-label="Next month">&#8250;</button>
      </div>
      <div class="drm-weekdays"><span>S</span><span>M</span><span>T</span><span>W</span><span>T</span><span>F</span><span>S</span></div>
      <div class="drm-grid" id="drmGridRight"></div>
    </div>
  </div>

  <div class="drm-footer">
    <span class="drm-summary" id="drmSummary">Select a check-in date</span>
    <button type="button" class="drm-confirm" id="drmConfirm" disabled>Confirm dates</button>
  </div>
</div>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

What's included

Features

Two adjacent month calendars sharing one range-selection state machine
Three-click cycle: pick start, pick end, click again to start a fresh range
Live hover preview highlights in-between days before the end date is confirmed
Hover preview and confirmed range share the exact same highlighting logic
Past dates are genuinely disabled with no click handler attached, not just styled
Real millisecond-based nights calculation, correct across any month boundary
Confirm button disables until a complete start-and-end range is selected
Closures correctly capture each calendar day's own date for click and hover handlers
Export as HTML file, React JSX, or React + Tailwind CSS
Mobile (375px), Tablet (768px), Desktop device preview buttons

About this UI Snippet

Date Range Picker Modal — Two-Month Calendar with Hover Preview and a Real Nights Count

Screenshot of the Booking Date Range Picker Modal snippet rendered live

Booking flows need more than a single date field — they need a start and an end, with the days in between visually obvious before the second click even happens. This modal renders two adjacent month calendars sharing one range-selection state machine, previewing the in-between days on hover and computing a real nights count once both dates are picked.

A three-step click cycle, not just two independent date fields

selectDate(date) implements the standard range-picker cycle: if there's no rangeStart yet, or if a complete range already exists, the clicked date becomes a fresh rangeStart and any previous range clears. Otherwise, if the clicked date is before the existing rangeStart, it replaces the start (handling a visitor who picked a check-in, then changed their mind to an earlier date). Otherwise it becomes rangeEnd. This single function is the entire range logic — there's no separate "am I picking start or end" flag to keep in sync with it.

Hover preview reuses the exact same in-range check as the final range

While rangeStart is set but rangeEnd isn't, moving the mouse over a day sets hoverDate and re-renders. buildMonthGrid() computes previewEnd = rangeEnd || hoverDate once, then applies the identical date > rangeStart && date < previewEnd check whether previewEnd came from a confirmed end date or a live hover — so the preview highlighting a visitor sees before their second click is pixel-for-pixel the same styling the confirmed range will show afterward, not a separate lighter-weight preview state.

Past dates are genuinely disabled, not just styled to look that way

Every day before today gets both the .drm-day-disabled class and a real disabled attribute on the <button> — no click handler is even attached to a disabled day, so a booking cannot start or end in the past no matter how a visitor interacts with the calendar, not merely a visual style discouraging it.

Closures capture each day's own date correctly

Both the click and mouseenter handlers are bound via an immediately-invoked function that takes date as a parameter and returns the real handler — the same pattern needed anywhere event listeners are created inside a loop, since without it every handler would close over the loop's final date value instead of its own day.

The nights count is real arithmetic, not a lookup table

Math.round((rangeEnd - rangeStart) / 86400000) converts the millisecond difference between two Date objects into whole days by dividing by the number of milliseconds in a day — correct for any pair of dates regardless of the month or months they span, including a range that crosses a month boundary between the two visible calendars.

Customizing it

Add a maximum-range limit (e.g. a 30-night cap for a booking product) by adding a check inside selectDate()'s else branch that rejects an end date too far from rangeStart. Restyle .drm-day-in-range to your brand's accent color, and adjust the today cutoff logic if you need a minimum lead time (e.g. bookings must start at least 2 days out) instead of allowing "today" itself.

Step by step

How to Use

  1. 1
    Open the date pickerClick "Select dates" to open the two-month calendar modal.
  2. 2
    Click a start dateThe clicked day highlights and the summary asks for a check-out date.
  3. 3
    Hover before your second clickThe days between your start date and the hovered day preview-highlight live.
  4. 4
    Click an end dateThe range locks in, the summary shows a real computed nights count, and Confirm enables.
  5. 5
    Navigate monthsUse the arrow buttons to move both calendars forward or back one month together.
  6. 6
    Export in your formatClick "HTML" for a standalone file, "JSX" for a React component, or "Tailwind" for a React + Tailwind version.

Real-world uses

Common Use Cases

Hotel, rental, and travel booking flows
Let a guest pick check-in and check-out dates with a real nights count before proceeding to payment.
Equipment and venue rental platforms
Pair with a pricing calculator that multiplies the selected nights by a nightly rate.
Project and reporting date-range filters
Reuse the same two-month range picker for filtering analytics or reports by a custom date window.
Learn range-selection state machine patterns
Study how one selectDate() function handles the full start-then-end-then-reset click cycle without extra mode flags.
Event and appointment scheduling tools
Adapt the calendar grid for multi-day event scheduling with the same hover-preview technique.
Related: Date Range Picker
See the Date Range Picker for an inline, non-modal version of this same range-selection pattern.

Got questions?

Frequently Asked Questions

selectDate() checks if the clicked date is earlier than the current rangeStart, and if so, replaces rangeStart with it rather than treating it as an invalid click — this handles a visitor who picked a check-in date and then decided on an even earlier one, without forcing them to restart the whole selection.

buildMonthGrid() computes previewEnd as rangeEnd if it exists, otherwise hoverDate, then applies the same date > rangeStart && date < previewEnd check either way. Because both the live hover and the confirmed range flow through this identical check, the preview highlighting looks and behaves exactly like the final confirmed range, just before the second click has happened.

No — every calendar day before today gets both a disabled CSS class and a real disabled attribute on its button element, so no click event listener is even attached to it. This is enforced at the DOM level, not just visually discouraged with styling.

Math.round((rangeEnd - rangeStart) / 86400000) subtracts the two Date objects (which produces a millisecond difference) and divides by 86,400,000 — the number of milliseconds in a day — to get a whole number of nights. This works correctly for any two dates, including a range that spans across the month boundary between the two visible calendars.

selectDate() checks rangeStart && rangeEnd as a combined condition alongside "no rangeStart yet," and if a complete range already exists, the newly clicked date starts an entirely fresh range — clearing both the old start and end — rather than trying to extend or replace just one side of the existing range.

Yes — leftMonth is the only tracked month state, and rightMonth is always computed as one month after it inside renderCalendars(). The prev/next arrow buttons move leftMonth by one month and re-render both calendars together, so they can never fall out of consecutive alignment.