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
onPageLoading
lifecycle event.
jsxconst min = '2026-10-29T00:00';
const max = '2027-04-29T00:00';
const [singleLabels, setSingleLabels] = React.useState([]);
const [singleInvalid, setSingleInvalid] = React.useState([]);
const onPageLoadingSingle = React.useCallback((event) => {
getPrices(event.firstDay, (bookings) => {
setSingleLabels(bookings.labels);
setSingleInvalid(bookings.invalid);
});
}, []);
const getPrices = (d, callback) => {
const invalid = [];
const labels = [];
getJson('//trial.mobiscroll.com/getprices/?year=' + d.getFullYear() + '&month=' + d.getMonth(), (bookings) => {
for (const booking of bookings) {
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, invalid });
}, 'jsonp');
}
<Datepicker
controls={['calendar']}
display="inline"
min={min}
max={max}
labels={singleLabels}
invalid={singleInvalid}
onPageLoading={onPageLoadingSingle}
/>
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.
jsx// example for today's labels and invalids
const myLabels = React.useMemo(() => {
return [{
start: ' ',
textColor: '#e1528f',
title: ' '
}];
}, []);
const myInvalid = React.useMemo(() => {
return [{
start: '2026-10-29T08:00',
end: '2026-10-29T13:00'
}, {
start: '2026-10-29T15:00',
end: '2026-10-29T17:00'
}, {
start: '2026-10-29T19:00',
end: '2026-10-29T20:00'
}];
}, []);
<Datepicker
controls={['calendar', 'timegrid']}
display="inline"
min="2026-10-29T00:00"
max="2027-04-29T00:00"
minTime="08:00"
maxTime="19:59"
stepMinute={60}
labels={myLabels}
invalid={myInvalid}
/>
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.
jsxconst multiple = React.useMemo(() => {
return [
'2026-10-11T00:00',
'2026-10-16T00:00',
'2026-10-17T00:00'
]
}, []);
const min = '2026-10-29T00:00';
const max = '2027-04-29T00:00';
const [multipleLabels, setMultipleLabels] = React.useState([]);
const [multipleInvalid, setMultipleInvalid] = React.useState([]);
const onPageLoadingMultiple = React.useCallback((event, inst) => {
getBookings(event.firstDay, (bookings) => {
setMultipleLabels(bookings.labels);
setMultipleInvalid(bookings.invalid);
});
}, []);
const getBookings = (date, callback) => {
const invalid = [];
const labels = [];
getJson('//trial.mobiscroll.com/getbookings/?year=' + date.getFullYear() + '&month=' + date.getMonth(), (bookings) => {
for (const booking of bookings) {
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, invalid });
}, 'jsonp');
}
<Datepicker
controls={['calendar']}
display="inline"
value={multiple}
min={min}
max={max}
labels={multipleLabels}
invalid={multipleInvalid}
selectMultiple={true}
onPageLoading={onPageLoadingMultiple}
/>
import { Datepicker, getJson, Page, setOptions } from '@mobiscroll/react';
import { useCallback, useState } from 'react';
setOptions({
locale: ,
theme: ' ',
themeVariant: ' '
});
function App() {
const [multiple, setMultiple] = useState(['2026-10-11T00:00', '2026-10-16T00:00', '2026-10-17T00:00']);
const min = '2026-10-29T00:00';
const max = '2027-04-29T00:00';
const [singleLabels, setSingleLabels] = useState([]);
const [singleInvalid, setSingleInvalid] = useState([]);
const [datetimeLabels, setDatetimeLabels] = useState([]);
const [datetimeInvalid, setDatetimeInvalid] = useState([]);
const [multipleLabels, setMultipleLabels] = useState([]);
const [multipleInvalid, setMultipleInvalid] = useState([]);
const handlePageLoadingSingle = useCallback((args) => {
const d = args.firstDay;
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);
}
}
setSingleLabels(labels);
setSingleInvalid(invalid);
},
'jsonp',
);
}, []);
const handlePageLoadingDatetime = useCallback((args) => {
const d = args.firstDay;
const 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 d = new Date(booking.d);
if (booking.nr > 0) {
labels.push({
start: d,
title: booking.nr + ' SPOTS',
textColor: '#e1528f',
});
invalid.push(...booking.invalid);
} else {
invalid.push(d);
}
}
setDatetimeLabels(labels);
setDatetimeInvalid(invalid);
},
'jsonp',
);
}, []);
const handlePageLoadingMultiple = useCallback((args) => {
const d = args.firstDay;
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);
}
}
setMultipleLabels(labels);
setMultipleInvalid(invalid);
},
'jsonp',
);
}, []);
const handleChangeMultiple = useCallback((args) => {
setMultiple(args.value);
}, []);
return (
<Page className="md-calendar-booking">
<div className="mbsc-form-group">
<div className="mbsc-form-group-title">Single date & appointment booking</div>
<Datepicker
display="inline"
controls={['calendar']}
min={min}
max={max}
labels={singleLabels}
invalid={singleInvalid}
pages="auto"
onPageLoading={handlePageLoadingSingle}
/>
</div>
<div className="mbsc-form-group">
<div className="mbsc-form-group-title">Select date & time</div>
<Datepicker
display="inline"
controls={['calendar', 'timegrid']}
min={min}
max={max}
minTime="08:00"
maxTime="19:59"
stepMinute={60}
labels={datetimeLabels}
invalid={datetimeInvalid}
onPageLoading={handlePageLoadingDatetime}
cssClass="booking-datetime"
/>
</div>
<div className="mbsc-form-group">
<div className="mbsc-form-group-title">Booking multiple appointments</div>
<Datepicker
display="inline"
controls={['calendar']}
value={multiple}
min={min}
max={max}
labels={multipleLabels}
invalid={multipleInvalid}
pages="auto"
selectMultiple={true}
onChange={handleChangeMultiple}
onPageLoading={handlePageLoadingMultiple}
/>
</div>
</Page>
);
}
export default App;
import {
Datepicker,
getJson,
MbscCalendarLabel,
MbscDatepickerChangeEvent,
MbscDatepickerPageLoadingEvent,
MbscDateType,
Page,
setOptions ,
} from '@mobiscroll/react';
import { FC, useCallback, useState } from 'react';
setOptions({
locale: ,
theme: ' ',
themeVariant: ' '
});
const App: FC = () => {
const [multiple, setMultiple] = useState<MbscDateType[]>(['2026-10-11T00:00', '2026-10-16T00:00', '2026-10-17T00:00']);
const min = '2026-10-29T00:00';
const max = '2027-04-29T00:00';
const [singleLabels, setSingleLabels] = useState<MbscCalendarLabel[]>([]);
const [singleInvalid, setSingleInvalid] = useState<Array<object>>([]);
const [datetimeLabels, setDatetimeLabels] = useState<MbscCalendarLabel[]>([]);
const [datetimeInvalid, setDatetimeInvalid] = useState<Array<object>>([]);
const [multipleLabels, setMultipleLabels] = useState<MbscCalendarLabel[]>([]);
const [multipleInvalid, setMultipleInvalid] = useState<Array<object>>([]);
const handlePageLoadingSingle = useCallback((args: MbscDatepickerPageLoadingEvent) => {
const d = args.firstDay;
const invalid: Array<object> = [];
const labels: MbscCalendarLabel[] = [];
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);
}
}
setSingleLabels(labels);
setSingleInvalid(invalid);
},
'jsonp',
);
}, []);
const handlePageLoadingDatetime = useCallback((args: MbscDatepickerPageLoadingEvent) => {
const d = args.firstDay;
const invalid: Array<object> = [];
const labels: MbscCalendarLabel[] = [];
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 d = new Date(booking.d);
if (booking.nr > 0) {
labels.push({
start: d,
title: booking.nr + ' SPOTS',
textColor: '#e1528f',
});
invalid.push(...booking.invalid);
} else {
invalid.push(d);
}
}
setDatetimeLabels(labels);
setDatetimeInvalid(invalid);
},
'jsonp',
);
}, []);
const handlePageLoadingMultiple = useCallback((args: MbscDatepickerPageLoadingEvent) => {
const d = args.firstDay;
const invalid: Array<object> = [];
const labels: MbscCalendarLabel[] = [];
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);
}
}
setMultipleLabels(labels);
setMultipleInvalid(invalid);
},
'jsonp',
);
}, []);
const handleChangeMultiple = useCallback((args: MbscDatepickerChangeEvent) => {
setMultiple(args.value as MbscDateType[]);
}, []);
return (
<Page className="md-calendar-booking">
<div className="mbsc-form-group">
<div className="mbsc-form-group-title">Single date & appointment booking</div>
<Datepicker
display="inline"
controls={['calendar']}
min={min}
max={max}
labels={singleLabels}
invalid={singleInvalid}
pages="auto"
onPageLoading={handlePageLoadingSingle}
/>
</div>
<div className="mbsc-form-group">
<div className="mbsc-form-group-title">Select date & time</div>
<Datepicker
display="inline"
controls={['calendar', 'timegrid']}
min={min}
max={max}
minTime="08:00"
maxTime="19:59"
stepMinute={60}
labels={datetimeLabels}
invalid={datetimeInvalid}
onPageLoading={handlePageLoadingDatetime}
cssClass="booking-datetime"
/>
</div>
<div className="mbsc-form-group">
<div className="mbsc-form-group-title">Booking multiple appointments</div>
<Datepicker
display="inline"
controls={['calendar']}
value={multiple}
min={min}
max={max}
labels={multipleLabels}
invalid={multipleInvalid}
pages="auto"
selectMultiple={true}
onChange={handleChangeMultiple}
onPageLoading={handlePageLoadingMultiple}
/>
</div>
</Page>
);
};
export default App;
.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;
}