This is a booking component that I created toward the end of last year. It accepts an arrival date and departure date within a <form> which is then submitted to a third party booking engine via a query string URL.
I used flatpickr.js for the datepickers so that I could have more control over the way the datepicker looks and also some date handling features that flatpickr.js provides. It’s also keyboard accessible and complies with WCAG guidelines.
When the user selects an ‘Arrival date’ then the ‘Departure date’ will automatically be selected two days (this can easily be changed to another number) ahead of the user’s selected date. Or vice-versa, if the user selects a ‘Departure date’ first then the ‘Arrival date’ will be selected two days before the user’s selected date.
Users cannot select an ‘Arrival date’ in the past nor can they select the current date as the ‘Departure date’.
There is also some validation in place that will display an error message if either or both of the ‘Arrival date’ or ‘Departure date’ inputs are not selected upon submitting the form.
This particular booking component is configured to work with IHG booking engines, which has some odd requirements for submitting the month/day/year for the arrival and departure date query strings:
- The checkInDate/checkOutDate values must not have a leading zero.
- The checkInMonth/checkOutMonth values must be zero-based.
- The checkInMonth/checkOutMonth and checkInYear/checkOutYear values must be combined into respective checkInMonthYear/checkOutMonthYear values.
If you’re not using an IHG booking engine then you can remove the lines after the “IHG only!” comment. The booking component will work with most booking engines out of the box, assuming that they will simply accept a ‘YYYY-MM-DD’ format for the arrival and departure query string values. If not, you may need to modify the script and the <form> inputs as needed.
JavaScript
// Created by Alex Winter
// Last Modified: 2023-06-21
// *****************************************************
// Booking Form
// Flatpickr.js, selected date logic, validation, etc.
// *****************************************************
// allows for multiple booking forms by setting the "bookingId" parameter
const booking = (bookingId) => {
// Get DOM elements
const bookingForm = bookingId.querySelector('form');
const bookingSubmit = bookingId.querySelector('.booking-submit');
const arrival = bookingId.querySelector('.arrival');
const departure = bookingId.querySelector('.departure');
// default input placeholders
const arrivalPlaceholder = 'Arrival';
const departurePlaceholder = 'Departure';
arrival.setAttribute('placeholder', arrivalPlaceholder);
departure.setAttribute('placeholder', departurePlaceholder);
// arrival flatpickr instance
const arrivalFp = flatpickr(arrival, {
minDate: 'today', // ensures user cannot select a date prior to today
disableMobile: 'true', // disables native datepicker on mobile
monthSelectorType: 'static', // disables 'month' dropdown from flatpickr's datepicker (hope they add yearSelectorType soon)
altInput: true,
altFormat: "m/d/Y",
dateFormat: "Y-m-d",
onReady: (a, b, fp) => {
fp.altInput.setAttribute('aria-label', arrivalPlaceholder);
}
});
// departure flatpickr instance
const departureFp = flatpickr(departure, {
minDate: new Date().fp_incr(1), // ensures user cannot select a date prior to tomorrow
disableMobile: 'true', // disables native datepicker on mobile
monthSelectorType: 'static', // disables 'month' dropdown from flatpickr's datepicker (hope they add yearSelectorType soon)
altInput: true,
altFormat: "m/d/Y",
dateFormat: "Y-m-d",
onReady: (a, b, fp) => {
fp.altInput.setAttribute('aria-label', departurePlaceholder);
}
});
// add custom classes to the arrival and departure 'flatpickr-calendar' elements
arrivalFp.calendarContainer.classList.add('arrival-calendar');
departureFp.calendarContainer.classList.add('departure-calendar');
// flatpickr's date values
let arrivalDateChosen = arrivalFp.selectedDates;
let departureDateChosen = departureFp.selectedDates;
// js date objects
let arrivalDateObj = new Date(arrivalDateChosen);
let departureDateObj = new Date(departureDateChosen);
// update flatpickr's date values
const updateDates = () => {
// flatpickr date values
arrivalDateChosen = arrivalFp.selectedDates;
departureDateChosen = departureFp.selectedDates;
// js date objects
arrivalDateObj = new Date(arrivalDateChosen);
departureDateObj = new Date(departureDateChosen);
};
// run 'updateDates' function every time the user selects a new arrival or departure
[arrival, departure].forEach((item) => {
item.addEventListener('change', () => {
updateDates();
});
});
// amount of days to add or subtract
const defaultDaySpan = 2;
// add days to departure date
const addDays = () => {
departureDateObj = new Date(arrivalDateObj);
departureDateObj.setDate(arrivalDateObj.getDate() + defaultDaySpan)
departureFp.setDate(departureDateObj);
};
// subtract days from arrival date
const subDays = () => {
arrivalDateObj = new Date(departureDateObj);
arrivalDateObj.setDate(departureDateObj.getDate() - defaultDaySpan)
arrivalFp.setDate(arrivalDateObj);
};
// 86400000 milliseconds = 1 day
const minimumDistance = 86400000;
let dayDistance = 0;
// get the distance between days as a number of milliseconds
const getDayDistance = () => {
dayDistance = departureDateObj - arrivalDateObj;
};
// user selects arrival date
arrival.addEventListener('change', () => {
getDayDistance();
// if departure date is not selected yet - or -
// if arrival date is SAME DAY or AFTER currently selected departure date
if (!departureDateChosen.length || (departureDateChosen.length && dayDistance < minimumDistance)) {
addDays();
}
updateDates();
});
// user selects departure date
departure.addEventListener('change', () => {
getDayDistance();
// if arrival date is not selected yet - or -
// if departure date is SAME DAY or BEFORE currently selected arrival date
if (!arrivalDateChosen.length || (arrivalDateChosen.length && dayDistance < minimumDistance)) {
subDays();
};
updateDates();
});
// validate the form when the submit button is clicked
let errorMessageVisible = false; // prevents odd behaviour if user is button mashing
// create 'booking-error' element and get DOM elements
document.querySelector('body').insertAdjacentHTML('beforeend', '');
const error = document.querySelector('.booking-error');
const errorText = document.querySelector('.booking-error p');
// submit button is clicked
bookingSubmit.addEventListener('click', (event) => {
// show the error message for X seconds (if one isn't already shown)
const errorMessage = (message) => {
if (!errorMessageVisible) {
errorMessageVisible = true;
errorText.textContent = message;
error.classList.add('show');
setTimeout(function() {
errorMessageVisible = false;
error.classList.remove('show');
}, 4000)
};
event.preventDefault();
}
// validate: errors
if (arrivalDateChosen.length == 0 && departureDateChosen.length == 0) {
errorMessage('Please select an arrival and departure date');
}
else if (arrivalDateChosen.length == 0) {
errorMessage('Please select an arrival date');
}
else if (departureDateChosen.length == 0) {
errorMessage('Please select a departure date');
}
// validate: success
else {
// Google Analytics event tracking
try {
gtag('event', 'click', {
'event_category': 'Booking',
'event_label': 'Booking Form Submit'
});
}
catch(err) {}
// *****************************************************
// Split dates into separate variables
// Use only if needed
// *****************************************************
// regular expression that looks for YYYY-MM-DD
const regex = /(?\d{4})-(?\d{2})-(?\d{2})/;
// match 'arrival' and 'departure' with 'regex'
const regexArrival = arrival.value.match(regex);
const regexDeparture = departure.value.match(regex);
// split into separate variables
const arrivalY = regexArrival.groups.year;
const arrivalM = regexArrival.groups.month;
const arrivalD = regexArrival.groups.day;
const departureY = regexDeparture.groups.year;
const departureM = regexDeparture.groups.month;
const departureD = regexDeparture.groups.day;
// testing
/*
console.log('arrival: ' + arrival.value);
console.log('departure: ' + departure.value);
console.log('arrival year: ' + arrivalY);
console.log('arrival month: ' + arrivalM);
console.log('arrival day: ' + arrivalD);
console.log('departure year: ' + departureY);
console.log('departure month: ' + departureM);
console.log('departure day: ' + departureD);
*/
// *****************************************************
// IHG only!
// Comment out or delete as needed!
// *****************************************************
// IHG requires that the month value does not have a leading 0:
const checkInDate = parseInt(arrivalD, 10);
const checkOutDate = parseInt(departureD, 10);
// IHG requires that the month value is zero-based:
const checkInMonth = parseInt(arrivalM, 10) - 1;
const checkOutMonth = parseInt(departureM, 10) - 1;
// IHG requires the year value too of course
const checkInYear = arrivalY;
const checkOutYear = departureY;
// IHG requires that we combine the month value and the year value together
const checkInMonthYear = checkInMonth + checkInYear;
const checkOutMonthYear = checkOutMonth + checkOutYear;
// set the value for the 'checkInDate' input
document.querySelector('input[name="checkInDate"]').setAttribute('value', checkInDate);
// set the value for the 'checkOutDate' input
document.querySelector('input[name="checkOutDate"]').setAttribute('value', checkOutDate);
// set the value for the 'checkInMonthYear' input
document.querySelector('input[name="checkInMonthYear"]').setAttribute('value', checkInMonthYear);
// set the value for the 'checkInMonthYear' input
document.querySelector('input[name="checkOutMonthYear"]').setAttribute('value', checkOutMonthYear);
// testing
/*
console.log('checkInDate: ' + checkInDate);
console.log('checkOutDate: ' + checkOutDate);
console.log('checkInMonth: ' + checkInMonth);
console.log('checkOutMonth: ' + checkOutMonth);
console.log('checkInYear: ' + checkInYear);
console.log('checkOutYear: ' + checkOutYear);
console.log('checkInMonthYear: ' + checkInMonthYear);
console.log('checkOutMonthYear: ' + checkOutMonthYear);
*/
// *****************************************************
// Submit the form!
// *****************************************************
bookingForm.submit();
event.preventDefault();
};
});
};
/*
Initiate the booking function and select the appropriate HTML elements within the page by setting a booking ID
*/
const booking1 = document.querySelector('#booking-1');
if (booking1) {
booking(booking1)
}
/*
// Add additional booking instances if necessary:
const booking2 = document.querySelector('#booking-2');
if (booking2) {
booking(booking2)
}
const booking3 = document.querySelector('#booking-3');
if (booking3) {
booking(booking3)
}
// etc ...
*/
HTML
<div class="header-booking" id="booking-1" role="form" aria-label="Make a Reservation">
<form class="booking" name="booking" method="get" target="_blank" action="//ichotelsgroup.com/redirect">
<div class="booking-input date">
<input type="date" class="arrival" name="arrivaldate" aria-label="Arrival Date">
<i class="fa-regular fa-calendar-days"></i>
</div>
<div class="booking-input date">
<input type="date" class="departure" name="departuredate" aria-label="Departure Date">
<i class="fa-regular fa-calendar-days"></i>
</div>
<!-- for IHG booking engines only -->
<input type="hidden" name="checkInDate" value="">
<input type="hidden" name="checkOutDate" value="">
<input type="hidden" name="checkInMonthYear" value="">
<input type="hidden" name="checkOutMonthYear" value="">
<input type="hidden" name="path" value="rates">
<input type="hidden" name="brandCode" value="hi">
<input type="hidden" name="numberOfAdults" value="1">
<input type="hidden" name="numberOfChildren" value="0">
<input type="hidden" name="numberOfRooms" value="1">
<input type="hidden" name="rateCode" value="6CBARC">
<input type="hidden" name="hotelCode" value="dislb">
<input type="hidden" name="_PMID" value="99502222">
<div class="booking-input">
<button class="booking-submit">Book Now</button>
</div>
</form>
</div>
CSS
body {
margin: 0;
}
// Flatpickr Theme (Optimized)
// Last Modified: 2021-12-06 by AW
// v1.0
// CSS for the following was removed:
// time, date ranges, weeks, month dropdown, rtl, IE11 and other outdated browsers
// **************************************************
// .flatpickr-calendar
// **************************************************
.flatpickr-calendar {
position: absolute;
width: 100%;
max-width: 316px;
opacity: 0;
display: none;
visibility: hidden;
padding: 0.25em;
border: 0;
text-align: center;
direction: ltr;
animation: none;
box-sizing: border-box;
touch-action: manipulation;
background-color: #fff;
box-shadow: 1px 0 0 #e6e6e6, -1px 0 0 #e6e6e6, 0 1px 0 #e6e6e6, 0 -1px 0 #e6e6e6, 0 3px 13px rgba(0,0,0,0.08);
user-select: none;
}
.flatpickr-calendar.open {
opacity: 1;
max-height: 640px;
visibility: visible;
}
.flatpickr-calendar.open {
display: inline-block;
z-index: 100000001;
}
.flatpickr-calendar.animate.open {
animation: fpFadeInDown 300ms cubic-bezier(0.23, 1, 0.32, 1);
}
@keyframes fpFadeInDown {
from {
opacity: 0;
transform: translate3d(0, -20px, 0);
}
to {
opacity: 1;
transform: translate3d(0, 0, 0);
}
}
.flatpickr-calendar:focus {
outline: 0;
}
// **************************************************
// .flatpickr-months
// **************************************************
.flatpickr-months {
position: relative;
}
.flatpickr-months .flatpickr-month {
padding: 0.8em;
position: relative;
overflow: hidden;
background-color: #EDEAEA;
color: rgba(0,0,0,0.9);
user-select: none;
text-align: center;
}
// **************************************************
// .flatpickr-prev-month and .flatpickr-next-month
// **************************************************
.flatpickr-months .flatpickr-prev-month,
.flatpickr-months .flatpickr-next-month {
position: absolute;
top: 0;
height: 100%;
padding: 1em;
z-index: 3;
text-decoration: none;
cursor: pointer;
color: rgba(0,0,0,0.9);
}
.flatpickr-months .flatpickr-prev-month.flatpickr-disabled,
.flatpickr-months .flatpickr-next-month.flatpickr-disabled {
display: none;
}
.flatpickr-months .flatpickr-prev-month i,
.flatpickr-months .flatpickr-next-month i {
position: relative;
}
.flatpickr-months .flatpickr-prev-month.flatpickr-prev-month,
.flatpickr-months .flatpickr-next-month.flatpickr-prev-month {
left: 0;
}
.flatpickr-months .flatpickr-prev-month.flatpickr-next-month,
.flatpickr-months .flatpickr-next-month.flatpickr-next-month {
right: 0;
}
.flatpickr-months .flatpickr-prev-month:hover svg,
.flatpickr-months .flatpickr-next-month:hover svg {
fill: #3f9338;
}
.flatpickr-months .flatpickr-prev-month svg,
.flatpickr-months .flatpickr-next-month svg {
width: 14px;
height: 14px;
vertical-align: bottom;
}
.flatpickr-months .flatpickr-prev-month svg path,
.flatpickr-months .flatpickr-next-month svg path {
transition: fill 0.1s;
fill: inherit;
}
// **************************************************
// .flatpickr-current-month
// **************************************************
.flatpickr-current-month span.cur-month {
display: inline-block;
}
.flatpickr-current-month .numInputWrapper {
display: inline-block;
width: 6ch;
}
.flatpickr-current-month .numInputWrapper span.arrowUp:after {
border-bottom-color: rgba(0,0,0,0.9);
}
.flatpickr-current-month .numInputWrapper span.arrowDown:after {
border-top-color: rgba(0,0,0,0.9);
}
.flatpickr-current-month input.cur-year {
display: inline-block;
margin: 0;
background-color: transparent;
box-sizing: border-box;
cursor: text;
padding: 0.5ch 0 0.5ch 0.5ch;
border: 0;
appearance: textfield;
}
.flatpickr-current-month input.cur-year:focus {
outline: 0;
}
.flatpickr-current-month input.cur-year[disabled],
.flatpickr-current-month input.cur-year[disabled]:hover {
color: rgba(0,0,0,0.5);
background-color: transparent;
pointer-events: none;
}
// **************************************************
// .numInputWrapper
// **************************************************
.numInputWrapper {
position: relative;
height: auto;
}
.numInputWrapper input,
.numInputWrapper span {
display: inline-block;
}
.numInputWrapper input {
width: 100%;
}
.numInputWrapper input::-webkit-outer-spin-button,
.numInputWrapper input::-webkit-inner-spin-button {
margin: 0;
-webkit-appearance: none;
}
.numInputWrapper span {
position: absolute;
right: 0;
width: 14px;
padding: 0 4px 0 2px;
height: 50%;
line-height: 50%;
opacity: 0;
cursor: pointer;
border: 1px solid rgba(57,57,57,0.15);
box-sizing: border-box;
}
.numInputWrapper span:hover {
background-color: rgba(0,0,0,0.1);
}
.numInputWrapper span:active {
background-color: rgba(0,0,0,0.2);
}
.numInputWrapper span:after {
display: block;
content: "";
position: absolute;
}
.numInputWrapper span.arrowUp {
top: 0;
border-bottom: 0;
}
.numInputWrapper span.arrowUp:after {
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-bottom: 4px solid rgba(57,57,57,0.6);
top: 26%;
}
.numInputWrapper span.arrowDown {
top: 50%;
}
.numInputWrapper span.arrowDown:after {
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-top: 4px solid rgba(57,57,57,0.6);
top: 40%;
}
.numInputWrapper:hover {
background-color: rgba(0,0,0,0.05);
}
.numInputWrapper:hover span {
opacity: 1;
}
// **************************************************
// .flatpickr-innerContainer
// **************************************************
.flatpickr-innerContainer {
display: block;
display: flex;
box-sizing: border-box;
overflow: hidden;
}
// **************************************************
// .flatpickr-rContainer
// **************************************************
.flatpickr-rContainer {
display: inline-block;
padding: 0;
box-sizing: border-box;
}
// **************************************************
// .flatpickr-weekdays
// **************************************************
.flatpickr-weekdays {
display: flex;
align-items: center;
width: 100%;
padding: 0.9em 0;
font-size: 0.9rem;
background-color: transparent;
overflow: hidden;
text-align: center;
user-select: none;
}
.flatpickr-weekdays .flatpickr-weekdaycontainer {
display: flex;
flex: 1;
}
span.flatpickr-weekday {
cursor: default;
background-color: transparent;
color: #555;
line-height: 1;
margin: 0;
text-align: center;
display: block;
flex: 1;
font-weight: bolder;
}
// **************************************************
// .flatpickr-days
// **************************************************
.flatpickr-days {
position: relative;
overflow: hidden;
display: flex;
align-items: flex-start;
width: 307.875px;
user-select: none;
}
.flatpickr-days:focus {
outline: 0;
}
.dayContainer {
padding: 0;
outline: 0;
text-align: left;
width: 307.875px;
min-width: 307.875px;
max-width: 307.875px;
box-sizing: border-box;
display: inline-block;
display: flex;
flex-wrap: wrap;
justify-content: space-around;
transform: translate3d(0px, 0px, 0px);
opacity: 1;
}
.dayContainer + .dayContainer {
box-shadow: -1px 0 0 #e6e6e6;
}
.flatpickr-day {
background-color: #EDEAEA;
border: 1px solid transparent;
// border-radius: 150px;
box-sizing: border-box;
color: #393939;
cursor: pointer;
font-weight: 400;
width: 14.2857143%;
flex-basis: 14.2857143%;
max-width: 39px;
height: 39px;
line-height: 39px;
margin: 0 0 0.25em 0;
display: inline-block;
position: relative;
justify-content: center;
text-align: center;
}
.flatpickr-day:hover,
.flatpickr-day.prevMonthDay:hover,
.flatpickr-day.nextMonthDay:hover,
.flatpickr-day:focus,
.flatpickr-day.prevMonthDay:focus,
.flatpickr-day.nextMonthDay:focus {
cursor: pointer;
outline: 0;
color: #fff;
background-color: #3f9338;
border-color: #3f9338;
}
.flatpickr-day.today {
border-color: #959ea9;
}
.flatpickr-day.today:hover,
.flatpickr-day.today:focus {
border-color: #959ea9;
background-color: #959ea9;
color: #fff;
}
.flatpickr-day.selected,
.flatpickr-day.selected:focus,
.flatpickr-day.selected:hover,
.flatpickr-day.selected.prevMonthDay,
.flatpickr-day.selected.nextMonthDay {
box-shadow: none;
color: #fff;
background-color: #3f9338;
border-color: #3f9338;
}
.flatpickr-day.flatpickr-disabled,
.flatpickr-day.flatpickr-disabled:hover,
.flatpickr-day.prevMonthDay,
.flatpickr-day.nextMonthDay,
.flatpickr-day.notAllowed,
.flatpickr-day.notAllowed.prevMonthDay,
.flatpickr-day.notAllowed.nextMonthDay {
color: rgba(57,57,57,0.3);
background-color: transparent;
border-color: transparent;
cursor: default;
}
.flatpickr-day.flatpickr-disabled,
.flatpickr-day.flatpickr-disabled:hover {
cursor: not-allowed;
background-color: #F8F7F7;
color: rgba(57,57,57,0.2);
}
.flatpickr-day.hidden {
visibility: hidden;
}
// **************************************************
// .flatpickr-input
// **************************************************
.flatpickr-input[readonly] {
cursor: pointer;
}
// **************************************************
// Booking Form
// **************************************************
.header-booking {
margin-top: 4em;
}
.booking {
display: flex;
flex-direction: row;
justify-content: center;
gap: 1.25em;
flex-wrap: wrap;
@media (max-width: 1600px) {
gap: 0.25em;
}
}
.booking-input {
&.date,
&.select {
width: 100%;
max-width: 135px;
display: flex;
align-items: center;
column-gap: 0.5em;
}
input[type='text'],
input[type='date'],
select,
button {
width: 100%;
cursor: pointer;
border-radius: 3px;
}
input[type='text'],
input[type='date'],
select {
max-width: 105px;
color: #525252;
font-size: 0.9rem;
font-weight: 400;
padding: 0.5em;
background-color: #fff;
border: 1px solid c4c4c4;
box-shadow: inset 1px 1px 3px c4c4c4;
@media (max-width: 1600px) {
max-width: 95px;
}
}
input[type='text']::placeholder,
input[type='date']::placeholder {
color: #525252;
}
input[type='text']:hover,
input[type='date']:hover,
select:hover {
border-color: #c9c9c9;
}
input[type='date']:after {
content: '\f3cf';
font-family: 'FA-Solid';
}
select {
-webkit-appearance: none;
}
button {
display: inline-block;
padding-block: 0.7em;
padding-inline: 2.5em;
line-height: 1;
background-color: #3f9338;
border: 2px solid #3f9338;
border-radius: 0.25em;
color: #fff;
font-size: 1.1rem;
font-weight: 700;
font-style: normal;
text-align: center;
text-decoration: none;
text-transform: uppercase;
transition: all 0.5s;
@media (max-width: 1600px) {
padding-inline: 1.5em;
}
}
i {
color: #525252;
font-size: 1.35rem;
}
}
// **************************************************
// Banners
// **************************************************
%banner {
display: block;
width: 100%;
}
%banner-content {
position: relative;
padding: 0.65em 3em;
margin: 0 auto;
text-align: center;
}
%banner-text {
font-size: 1.2rem;
line-height: 1.4rem;
font-weight: 400;
color: #fff;
}
// **************************************************
// Banners: Booking Error
// **************************************************
.booking-error {
@extend %banner;
width: 100%;
max-width: 420px;
margin: 0 auto;
z-index: -1; // must always be less than 'banner' and 'header' element
background-color: red;
opacity: 0;
// position: fixed;
// inset: 4.5em 0 auto auto;
margin-top: 1em;
transition: all 1s;
&.show {
opacity: 1;
z-index: 30000; // must always be more than 'banner' and 'header' element
}
p {
color: #fff;
margin-bottom: 0;
}
}
.booking-error-content {
@extend %banner-content;
p {
@extend %banner-text;
margin: 0;
}
}