Appointment Booking
Book single or multiple appointments depending on the use-case or set up recurring date and time selection. Limit the available dates with the min and max options. Make dates un-selectable with the invalid option. Load pricing, available spots on demand and show it as labels that help people book the appointment.
The calendar can be displayed inline with one or more months or linked to an input. Learn how to initialize the calendar.
Demo
For enhanced validation set min and max dates. In addition to that disable invalid days by passing them to the invalid option.
If you need to show additional information, use the labels option and pass prices or availability. Invalids and things like pricing can be set on-load or on-demand in the
@page-loading
lifecycle event.
<script setup>
const minDate = '2026-11-04T00:00';
const maxDate = '2027-05-04T00:00';
const singleLabels = ref([])
const singleInvalid = ref([])
function handlePageLoadingSingle(event) {
getPrices(event.firstDay, (bookings) => {
singleLabels.value = bookings.labels
singleInvalid.value = bookings.invalid
})
}
function getPrices(d, callback) {
let invalid = []
let labels = []
getJson(
'https://trial.mobiscroll.com/getprices/?year=' + d.getFullYear() + '&month=' + d.getMonth(),
(bookings) => {
for (let i = 0; i < bookings.length; ++i) {
const booking = bookings[i]
const d = new Date(booking.d)
if (booking.price > 0) {
labels.push({
start: d,
title: '$' + booking.price,
textColor: '#e1528f'
})
} else {
invalid.push(d)
}
}
callback({ labels: labels, invalid: invalid })
},
'jsonp'
)
}
</script>
<template>
<MbscDatepicker
display="inline"
:controls="['calendar']"
:min="minDate"
:max="maxDate"
:labels="singleLabels"
:invalid="singleInvalid"
@page-loading="handlePageLoadingSingle"
/>
</template>
Use a calendar in combination with a time picker or timegrid. Dynamically filter the time grid based on availability and add labels to the dates to inform users of free spots.
<script setup>
const minDate = '2026-11-04T00:00';
const maxDate = '2027-05-04T00:00';
const datetimeLabels = ref([])
const datetimeInvalid = ref([])
function handlePageLoadingDatetime(event) {
getPrices(event.firstDay, (bookings) => {
datetimeLabels.value = bookings.labels
datetimeInvalid.value = bookings.invalid
})
}
function getDatetimes(d, callback) {
let invalid = []
let labels = []
getJson(
'https://trial.mobiscroll.com/getbookingtime/?year=' +
d.getFullYear() +
'&month=' +
d.getMonth(),
(bookings) => {
for (let i = 0; i < bookings.length; ++i) {
const booking = bookings[i]
const bDate = new Date(booking.d)
if (booking.nr > 0) {
labels.push({
start: bDate,
title: booking.nr + ' SPOTS',
textColor: '#e1528f'
})
invalid = [...invalid, ...booking.invalid]
} else {
invalid.push(d)
}
}
callback({ labels: labels, invalid: invalid })
},
'jsonp'
)
}
</script>
<template>
<MbscDatepicker
display="inline"
minTime="08:00"
maxTime="19:59"
:controls="['calendar', 'timegrid']"
:min="minDate"
:max="maxDate"
:stepMinute="60"
:labels="datetimeLabels"
:invalid="datetimeInvalid"
@page-loading="handlePageLoadingDatetime"
/>
</template>
For multiple event booking set the selectMultiple option to 'true'. min, max, invalid are essential for a great UX and labels can be used for showing extra information about available spots.
<script setup>
const multiple = [
'2026-11-11T00:00',
'2026-11-16T00:00',
'2026-11-17T00:00'
]
const minDate = '2026-11-04T00:00';
const maxDate = '2027-05-04T00:00';
const multipleLabels = ref([])
const multipleInvalid = ref([])
function handlePageLoadingMultiple(event) {
getBookings(event.firstDay, (bookings) => {
multipleLabels.value = bookings.labels
multipleInvalid.value = bookings.invalid
})
}
function getBookings(d, callback) {
let invalid = []
let labels = []
getJson(
'https://trial.mobiscroll.com/getbookings/?year=' + d.getFullYear() + '&month=' + d.getMonth(),
(bookings) => {
for (let i = 0; i < bookings.length; ++i) {
const booking = bookings[i]
const d = new Date(booking.d)
if (booking.nr > 0) {
labels.push({
start: d,
title: booking.nr + ' SPOTS',
textColor: '#e1528f'
})
} else {
invalid.push(d)
}
}
callback({ labels: labels, invalid: invalid })
},
'jsonp'
)
}
</script>
<template>
<MbscDatepicker
v-model="multiple"
display="inline"
:controls="['calendar']"
:min="minDate"
:max="maxDate"
:labels="multipleLabels"
:invalid="multipleInvalid"
:selectMultiple="true"
@page-loading="handlePageLoadingMultiple"
/>
</template>
<script setup>
import { getJson, MbscDatepicker, MbscPage, setOptions } from '@mobiscroll/vue'
import { ref } from 'vue'
setOptions({
locale: ,
theme: ' ',
themeVariant: ' '
})
const minDate = '2026-11-04T00:00'
const maxDate = '2027-05-04T00:00'
const multiple = ref(['2026-11-11T00:00', '2026-11-16T00:00', '2026-11-17T00:00'])
const singleLabels = ref([])
const singleInvalid = ref([])
const datetimeLabels = ref([])
const datetimeInvalid = ref([])
const multipleLabels = ref([])
const multipleInvalid = ref([])
function handlePageLoadingSingle(event) {
getPrices(event.firstDay, (bookings) => {
singleLabels.value = bookings.labels
singleInvalid.value = bookings.invalid
})
}
function handlePageLoadingDatetime(event) {
getDatetimes(event.firstDay, (bookings) => {
datetimeLabels.value = bookings.labels
datetimeInvalid.value = bookings.invalid
})
}
function handlePageLoadingMultiple(event) {
getBookings(event.firstDay, (bookings) => {
multipleLabels.value = bookings.labels
multipleInvalid.value = bookings.invalid
})
}
function getPrices(d, callback) {
const invalid = []
const labels = []
getJson(
'https://trial.mobiscroll.com/getprices/?year=' + d.getFullYear() + '&month=' + d.getMonth(),
(bookings) => {
for (let i = 0; i < bookings.length; ++i) {
const booking = bookings[i]
const d = new Date(booking.d)
if (booking.price > 0) {
labels.push({
start: d,
title: '$' + booking.price,
textColor: '#e1528f'
})
} else {
invalid.push(d)
}
}
callback({ labels: labels, invalid: invalid })
},
'jsonp'
)
}
function getDatetimes(d, callback) {
let invalid = []
const labels = []
getJson(
'https://trial.mobiscroll.com/getbookingtime/?year=' +
d.getFullYear() +
'&month=' +
d.getMonth(),
(bookings) => {
for (let i = 0; i < bookings.length; ++i) {
const booking = bookings[i]
const bDate = new Date(booking.d)
if (booking.nr > 0) {
labels.push({
start: bDate,
title: booking.nr + ' SPOTS',
textColor: '#e1528f'
})
invalid = [...invalid, ...booking.invalid]
} else {
invalid.push(d)
}
}
callback({ labels: labels, invalid: invalid })
},
'jsonp'
)
}
function getBookings(d, callback) {
const invalid = []
const labels = []
getJson(
'https://trial.mobiscroll.com/getbookings/?year=' + d.getFullYear() + '&month=' + d.getMonth(),
(bookings) => {
for (let i = 0; i < bookings.length; ++i) {
const booking = bookings[i]
const d = new Date(booking.d)
if (booking.nr > 0) {
labels.push({
start: d,
title: booking.nr + ' SPOTS',
textColor: '#e1528f'
})
} else {
invalid.push(d)
}
}
callback({ labels: labels, invalid: invalid })
},
'jsonp'
)
}
</script>
<template>
<MbscPage>
<div class="dms-calendar-booking">
<div class="mbsc-form-group">
<div class="mbsc-form-group-title">Single date & appointment booking</div>
<MbscDatepicker
display="inline"
pages="auto"
:controls="['calendar']"
:min="minDate"
:max="maxDate"
:labels="singleLabels"
:invalid="singleInvalid"
@page-loading="handlePageLoadingSingle"
/>
</div>
<div class="mbsc-form-group">
<div class="mbsc-form-group-title">Select date & time</div>
<MbscDatepicker
display="inline"
minTime="08:00"
maxTime="19:59"
cssClass="booking-datetime"
:controls="['calendar', 'timegrid']"
:min="minDate"
:max="maxDate"
:stepMinute="60"
:labels="datetimeLabels"
:invalid="datetimeInvalid"
@page-loading="handlePageLoadingDatetime"
/>
</div>
<div class="mbsc-form-group">
<div class="mbsc-form-group-title">Booking multiple appointments</div>
<MbscDatepicker
display="inline"
pages="auto"
cssClass="booking-datetime"
v-model="multiple"
:controls="['calendar']"
:min="minDate"
:max="maxDate"
:stepMinute="60"
:selectMultiple="true"
:labels="multipleLabels"
:invalid="multipleInvalid"
@page-loading="handlePageLoadingMultiple"
/>
</div>
</div>
</MbscPage>
</template>
<style>
.md-calendar-booking .mbsc-calendar-text {
text-align: center;
}
.md-calendar-booking .booking-datetime .mbsc-datepicker-tab-calendar {
flex: 1 1 0;
min-width: 300px;
}
.md-calendar-booking .mbsc-timegrid-item {
margin-top: 1.5em;
margin-bottom: 1.5em;
}
.md-calendar-booking .mbsc-timegrid-container {
top: 30px;
}
</style><script setup lang="ts">
import { getJson, MbscDatepicker, MbscPage, setOptions } from '@mobiscroll/vue'
import { ref } from 'vue'
setOptions({
locale: ,
theme: ' ',
themeVariant: ' '
})
const minDate = '2026-11-04T00:00'
const maxDate = '2027-05-04T00:00'
const multiple = ref(['2026-11-11T00:00', '2026-11-16T00:00', '2026-11-17T00:00'])
const singleLabels = ref([])
const singleInvalid = ref([])
const datetimeLabels = ref([])
const datetimeInvalid = ref([])
const multipleLabels = ref([])
const multipleInvalid = ref([])
function handlePageLoadingSingle(event: any) {
getPrices(event.firstDay, (bookings: any) => {
singleLabels.value = bookings.labels
singleInvalid.value = bookings.invalid
})
}
function handlePageLoadingDatetime(event: any) {
getDatetimes(event.firstDay, (bookings: any) => {
datetimeLabels.value = bookings.labels
datetimeInvalid.value = bookings.invalid
})
}
function handlePageLoadingMultiple(event: any) {
getBookings(event.firstDay, (bookings: any) => {
multipleLabels.value = bookings.labels
multipleInvalid.value = bookings.invalid
})
}
function getPrices(d: Date, callback: any) {
const invalid: any[] = []
const labels: any[] = []
getJson(
'https://trial.mobiscroll.com/getprices/?year=' + d.getFullYear() + '&month=' + d.getMonth(),
(bookings) => {
for (let i = 0; i < bookings.length; ++i) {
const booking = bookings[i]
const d = new Date(booking.d)
if (booking.price > 0) {
labels.push({
start: d,
title: '$' + booking.price,
textColor: '#e1528f'
})
} else {
invalid.push(d)
}
}
callback({ labels: labels, invalid: invalid })
},
'jsonp'
)
}
function getDatetimes(d: Date, callback: any) {
let invalid: any[] = []
const labels: any[] = []
getJson(
'https://trial.mobiscroll.com/getbookingtime/?year=' +
d.getFullYear() +
'&month=' +
d.getMonth(),
(bookings) => {
for (let i = 0; i < bookings.length; ++i) {
const booking = bookings[i]
const bDate = new Date(booking.d)
if (booking.nr > 0) {
labels.push({
start: bDate,
title: booking.nr + ' SPOTS',
textColor: '#e1528f'
})
invalid = [...invalid, ...booking.invalid]
} else {
invalid.push(d)
}
}
callback({ labels: labels, invalid: invalid })
},
'jsonp'
)
}
function getBookings(d: Date, callback: any) {
const invalid: any[] = []
const labels: any[] = []
getJson(
'https://trial.mobiscroll.com/getbookings/?year=' + d.getFullYear() + '&month=' + d.getMonth(),
(bookings) => {
for (let i = 0; i < bookings.length; ++i) {
const booking = bookings[i]
const d = new Date(booking.d)
if (booking.nr > 0) {
labels.push({
start: d,
title: booking.nr + ' SPOTS',
textColor: '#e1528f'
})
} else {
invalid.push(d)
}
}
callback({ labels: labels, invalid: invalid })
},
'jsonp'
)
}
</script>
<template>
<MbscPage>
<div class="dms-calendar-booking">
<div class="mbsc-form-group">
<div class="mbsc-form-group-title">Single date & appointment booking</div>
<MbscDatepicker
display="inline"
pages="auto"
:controls="['calendar']"
:min="minDate"
:max="maxDate"
:labels="singleLabels"
:invalid="singleInvalid"
@page-loading="handlePageLoadingSingle"
/>
</div>
<div class="mbsc-form-group">
<div class="mbsc-form-group-title">Select date & time</div>
<MbscDatepicker
display="inline"
minTime="08:00"
maxTime="19:59"
cssClass="booking-datetime"
:controls="['calendar', 'timegrid']"
:min="minDate"
:max="maxDate"
:stepMinute="60"
:labels="datetimeLabels"
:invalid="datetimeInvalid"
@page-loading="handlePageLoadingDatetime"
/>
</div>
<div class="mbsc-form-group">
<div class="mbsc-form-group-title">Booking multiple appointments</div>
<MbscDatepicker
display="inline"
pages="auto"
cssClass="booking-datetime"
v-model="multiple"
:controls="['calendar']"
:min="minDate"
:max="maxDate"
:stepMinute="60"
:selectMultiple="true"
:labels="multipleLabels"
:invalid="multipleInvalid"
@page-loading="handlePageLoadingMultiple"
/>
</div>
</div>
</MbscPage>
</template>
<style>
.md-calendar-booking .mbsc-calendar-text {
text-align: center;
}
.md-calendar-booking .booking-datetime .mbsc-datepicker-tab-calendar {
flex: 1 1 0;
min-width: 300px;
}
.md-calendar-booking .mbsc-timegrid-item {
margin-top: 1.5em;
margin-bottom: 1.5em;
}
.md-calendar-booking .mbsc-timegrid-container {
top: 30px;
}
</style>