Appointment Scheduling
Usage
theme="ios" You can turn auto theming on and let it decide based on the platform.
Usage
themeVariant="light" You can turn auto dark mode on based on system settings.
Usage
clickToCreate={true}
// click to create is on dragToCreate={true}
// drag to create is on dragToMove={true}
// reordering is on dragToResize={true}
// resizing is on eventDelete={true}
// deleting is on Fine-tune drag & drop with these options, which are false by default.
Usage
locale={localeEn} Selecting the locale loads the localized settings and the translated UI text.
Appointment Scheduling
The scheduler can be set up for scheduling/unscheduling booked appointments. Simply set up a daily schedule view with multiple resources representing dentists or doctors. Next to the calendar you can render a list of unscheduled appointments.
Enabling advanced validation and combining that with the drag & drop between the calendar and external list on a resource to resource basis you got a pretty nice working base for appointment scheduling that can be further customized.
Demo
The live demo didn't load.
Try it on demo.mobiscroll.com
Code
View code on GitHub
import { Draggable, Dropcontainer, Eventcalendar, setOptions, Toast } from '@mobiscroll/react';
import PropTypes from 'prop-types';
import { useCallback, useMemo, useState } from 'react';
setOptions({
locale: ,
theme: ' ',
themeVariant: ' '
});
const now = new Date();
const today = new Date(now.setMinutes(59));
const yesterday = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1);
const Appointment = (props) => {
const [draggable, setDraggable] = useState();
const setDragElm = useCallback((elm) => {
setDraggable(elm);
}, []);
const event = props.data;
const eventLength = Math.abs(new Date(event.end).getTime() - new Date(event.start).getTime()) / (60 * 60 * 1000);
return (
<div>
{!event.hide && (
<div ref={setDragElm} className="docs-appointment-task" style={{ background: event.color }}>
<div>{event.title}</div>
<div>{eventLength + ' hour' + (eventLength > 1 ? 's' : '')}</div>
<Draggable dragData={event} element={draggable} />
</div>
)}
</div>
);
};
Appointment.propTypes = {
data: PropTypes.object.isRequired,
};
function App() {
const doctors = useMemo(
() => [
{ id: 1, name: 'Dr. Keila Delores' },
{ id: 2, name: 'Dr. Gene Cortez' },
{ id: 3, name: 'Dr. Paula Bush' },
{ id: 4, name: 'Dr. Pete Nichols' },
{ id: 5, name: 'Dr. Jean Pearson' },
{ id: 6, name: 'Dr. Thelma Cain' },
],
[],
);
const myView = useMemo(
() => ({
scheduler: {
type: 'day',
startTime: '08:00',
endTime: '20:00',
allDay: false,
},
}),
[],
);
const myInvalid = useMemo(() => [{ recurring: { repeat: 'daily', until: yesterday } }, { start: yesterday, end: today }], []);
const [myEvents, setEvents] = useState(
[
{
id: 'job1',
start: '2026-10-24T14:00',
end: '2026-10-24T16:00',
resource: 1,
title: 'Myla Bennett',
job: 'Wisdom tooth removal',
color: '#334ab9',
},
{
id: 'job2',
start: '2026-10-24T17:00',
end: '2026-10-24T18:30',
resource: 1,
title: 'Beatrix Foley',
job: 'Braces',
color: '#177e70',
},
{
id: 'job3',
start: '2026-10-24T08:00',
end: '2026-10-24T09:30',
resource: 3,
title: 'Frank Watson',
job: 'Teeth whitening',
color: '#d1891f',
},
{
id: 'job4',
start: '2026-10-24T10:00',
end: '2026-10-24T12:30',
resource: 3,
title: 'Jaime Joyce',
job: 'Root canal treatment',
color: '#cb3939',
},
{
id: 'job5',
start: '2026-10-24T13:00',
end: '2026-10-24T14:00',
resource: 3,
title: 'Corey Shepard',
job: 'Tooth extraction',
color: '#aba343',
},
{
id: 'job6',
start: '2026-10-24T14:00',
end: '2026-10-24T16:00',
resource: 4,
title: 'Callie Leonard',
job: 'Crown and bridge',
color: '#1ca11a',
},
{
id: 'job7',
start: '2026-10-24T17:00',
end: '2026-10-24T18:00',
resource: 4,
title: 'Harley Thomson',
job: 'Tartar removal',
color: '#a446b5',
},
{
id: 'job8',
start: '2026-10-24T09:00',
end: '2026-10-24T11:00',
resource: 6,
title: 'Ricky Welch',
job: 'Wisdom tooth removal',
color: '#334ab9',
},
].map((event) => {
const start = event.start ? new Date(event.start) : event.start;
const end = event.end ? new Date(event.end) : event.end;
return {
...event,
start,
end,
// Mark past events as fixed by setting the event.editable property to false
editable: !!(start && today < start),
};
}),
);
const [appointments, setAppointments] = useState([
{
id: 'd1',
title: 'Winfred Lesley',
job: 'Teeth whitening',
color: '#d1891f',
start: '2026-10-24T08:00',
end: '2026-10-24T09:30',
unscheduled: true,
},
{
id: 'd2',
title: 'Rosalin Delice',
job: 'Crown and bridge',
color: '#1ca11a',
start: '2026-10-24T08:00',
end: '2026-10-24T10:00',
unscheduled: true,
},
{
id: 'd3',
title: 'Macy Steven',
job: 'Root canal treatment',
color: '#cb3939',
start: '2026-10-24T10:00',
end: '2026-10-24T12:30',
unscheduled: true,
},
{
id: 'd4',
title: 'Lavern Cameron',
job: 'Tartar removal',
color: '#a446b5',
start: '2026-10-24T12:00',
end: '2026-10-24T13:00',
unscheduled: true,
},
]);
const [contBg, setContBg] = useState('');
const [myColors, setColors] = useState([]);
const [dropCont, setDropCont] = useState();
const [toastMessage, setToastMessage] = useState('');
const [isToastOpen, setToastOpen] = useState(false);
const setDropElm = useCallback((elm) => {
setDropCont(elm);
}, []);
const handleEventCreate = useCallback((args) => {
const event = args.event;
event.unscheduled = false;
setColors([]);
}, []);
const handleEventCreated = useCallback((args) => {
setToastMessage(args.event.title + ' added');
setToastOpen(true);
setEvents((prevEvents) => [...prevEvents, args.event]);
setAppointments((prevAppointments) => prevAppointments.filter((item) => item.id !== args.event.id));
}, []);
const handleFailed = useCallback((event) => {
if (event.start <= today) {
setToastMessage("Can't add event in the past");
} else {
setToastMessage('Make sure not to double book');
}
setToastOpen(true);
}, []);
const handleEventCreateFailed = useCallback(
(args) => {
handleFailed(args.event);
},
[handleFailed],
);
const handleEventUpdateFailed = useCallback(
(args) => {
handleFailed(args.event);
},
[handleFailed],
);
const handleEventDelete = useCallback((args) => {
setToastMessage(args.event.title + ' unscheduled');
setToastOpen(true);
setEvents((prevEvents) => prevEvents.filter((item) => item.id !== args.event.id));
}, []);
const handleEventDragEnter = useCallback(() => {
setColors([
{
background: '#f1fff24d',
start: '08:00',
end: '20:00',
recurring: {
repeat: 'daily',
},
},
]);
}, []);
const handleEventDragLeave = useCallback(() => {
setColors([]);
}, []);
const handleItemDrop = useCallback((args) => {
if (args.data) {
args.data.unscheduled = true;
setAppointments((prevAppointments) => [...prevAppointments, args.data]);
}
setContBg('');
}, []);
const handleItemDragEnter = useCallback((args) => {
if (!(args.data && args.data.unscheduled)) {
setContBg('#d0e7d2cc');
}
}, []);
const handleItemDragLeave = useCallback(() => {
setContBg('');
}, []);
const handleToastClose = useCallback(() => {
setToastOpen(false);
}, []);
return (
<div className="mbsc-grid mbsc-no-padding">
<div className="mbsc-row">
<div className="mbsc-col-sm-9 docs-appointment-calendar">
<Eventcalendar
data={myEvents}
view={myView}
resources={doctors}
invalid={myInvalid}
dragToMove={true}
dragToCreate={true}
eventOverlap={false}
externalDrop={true}
externalDrag={true}
colors={myColors}
onEventCreate={handleEventCreate}
onEventCreated={handleEventCreated}
onEventCreateFailed={handleEventCreateFailed}
onEventUpdateFailed={handleEventUpdateFailed}
onEventDelete={handleEventDelete}
onEventDragEnter={handleEventDragEnter}
onEventDragLeave={handleEventDragLeave}
/>
<Toast isOpen={isToastOpen} message={toastMessage} onClose={handleToastClose} />
</div>
<div className="mbsc-col-sm-3 docs-appointment-cont" ref={setDropElm} style={{ backgroundColor: contBg }}>
<Dropcontainer
onItemDrop={handleItemDrop}
onItemDragEnter={handleItemDragEnter}
onItemDragLeave={handleItemDragLeave}
element={dropCont}
>
<div className="mbsc-form-group-title">Unscheduled appointments</div>
{appointments.map((app) => (
<Appointment key={app.id} data={app} />
))}
</Dropcontainer>
</div>
</div>
</div>
);
}
export default App;
import {
Draggable,
Dropcontainer,
Eventcalendar,
MbscCalendarColor,
MbscCalendarEvent,
MbscEventcalendarView,
MbscEventCreatedEvent,
MbscEventCreateEvent,
MbscEventCreateFailedEvent,
MbscEventDeleteEvent,
MbscEventUpdateFailedEvent,
MbscItemDragEvent,
MbscResource,
setOptions,
Toast ,
} from '@mobiscroll/react';
import { FC, useCallback, useMemo, useState } from 'react';
setOptions({
locale: ,
theme: ' ',
themeVariant: ' '
});
const now = new Date();
const today = new Date(now.setMinutes(59));
const yesterday = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1);
const Appointment: FC<{ data: MbscCalendarEvent }> = (props) => {
const [draggable, setDraggable] = useState<HTMLElement | null>();
const setDragElm = useCallback((elm: HTMLElement | null) => {
setDraggable(elm);
}, []);
const event = props.data;
const eventLength = Math.abs(new Date(event.end as string).getTime() - new Date(event.start as string).getTime()) / (60 * 60 * 1000);
return (
<div>
{!event.hide && (
<div ref={setDragElm} className="docs-appointment-task" style={{ background: event.color }}>
<div>{event.title}</div>
<div>{eventLength + ' hour' + (eventLength > 1 ? 's' : '')}</div>
<Draggable dragData={event} element={draggable} />
</div>
)}
</div>
);
};
const App: FC = () => {
const doctors = useMemo<MbscResource[]>(
() => [
{ id: 1, name: 'Dr. Keila Delores' },
{ id: 2, name: 'Dr. Gene Cortez' },
{ id: 3, name: 'Dr. Paula Bush' },
{ id: 4, name: 'Dr. Pete Nichols' },
{ id: 5, name: 'Dr. Jean Pearson' },
{ id: 6, name: 'Dr. Thelma Cain' },
],
[],
);
const myView = useMemo<MbscEventcalendarView>(
() => ({
scheduler: {
type: 'day',
startTime: '08:00',
endTime: '20:00',
allDay: false,
},
}),
[],
);
const myInvalid = useMemo(() => [{ recurring: { repeat: 'daily', until: yesterday } }, { start: yesterday, end: today }], []);
const [myEvents, setEvents] = useState<MbscCalendarEvent[]>([
{
id: 'job1',
start: '2026-10-24T14:00',
end: '2026-10-24T16:00',
resource: 1,
title: 'Myla Bennett',
job: 'Wisdom tooth removal',
color: '#334ab9',
},
{
id: 'job2',
start: '2026-10-24T17:00',
end: '2026-10-24T18:30',
resource: 1,
title: 'Beatrix Foley',
job: 'Braces',
color: '#177e70',
},
{
id: 'job3',
start: '2026-10-24T08:00',
end: '2026-10-24T09:30',
resource: 3,
title: 'Frank Watson',
job: 'Teeth whitening',
color: '#d1891f',
},
{
id: 'job4',
start: '2026-10-24T10:00',
end: '2026-10-24T12:30',
resource: 3,
title: 'Jaime Joyce',
job: 'Root canal treatment',
color: '#cb3939',
},
{
id: 'job5',
start: '2026-10-24T13:00',
end: '2026-10-24T14:00',
resource: 3,
title: 'Corey Shepard',
job: 'Tooth extraction',
color: '#aba343',
},
{
id: 'job6',
start: '2026-10-24T14:00',
end: '2026-10-24T16:00',
resource: 4,
title: 'Callie Leonard',
job: 'Crown and bridge',
color: '#1ca11a',
},
{
id: 'job7',
start: '2026-10-24T17:00',
end: '2026-10-24T18:00',
resource: 4,
title: 'Harley Thomson',
job: 'Tartar removal',
color: '#a446b5',
},
{
id: 'job8',
start: '2026-10-24T09:00',
end: '2026-10-24T11:00',
resource: 6,
title: 'Ricky Welch',
job: 'Wisdom tooth removal',
color: '#334ab9',
},
]);
const [appointments, setAppointments] = useState<MbscCalendarEvent[]>(
[
{
id: 'd1',
title: 'Winfred Lesley',
job: 'Teeth whitening',
color: '#d1891f',
start: '2026-10-24T08:00',
end: '2026-10-24T09:30',
unscheduled: true,
},
{
id: 'd2',
title: 'Rosalin Delice',
job: 'Crown and bridge',
color: '#1ca11a',
start: '2026-10-24T08:00',
end: '2026-10-24T10:00',
unscheduled: true,
},
{
id: 'd3',
title: 'Macy Steven',
job: 'Root canal treatment',
color: '#cb3939',
start: '2026-10-24T10:00',
end: '2026-10-24T12:30',
unscheduled: true,
},
{
id: 'd4',
title: 'Lavern Cameron',
job: 'Tartar removal',
color: '#a446b5',
start: '2026-10-24T12:00',
end: '2026-10-24T13:00',
unscheduled: true,
},
].map((event) => {
const start = event.start ? new Date(event.start as string) : event.start;
const end = event.end ? new Date(event.end as string) : event.end;
return {
...event,
start,
end,
// Mark past events as fixed by setting the event.editable property to false
editable: !!(start && today < start),
};
}),
);
const [contBg, setContBg] = useState('');
const [myColors, setColors] = useState<MbscCalendarColor[]>([]);
const [dropCont, setDropCont] = useState<HTMLElement | null>();
const [toastMessage, setToastMessage] = useState('');
const [isToastOpen, setToastOpen] = useState(false);
const setDropElm = useCallback((elm: HTMLElement | null) => {
setDropCont(elm);
}, []);
const handleEventCreate = useCallback((args: MbscEventCreateEvent) => {
const event = args.event;
event.unscheduled = false;
setColors([]);
}, []);
const handleEventCreated = useCallback((args: MbscEventCreatedEvent) => {
setToastMessage(args.event.title + ' added');
setToastOpen(true);
setEvents((prevEvents) => [...prevEvents, args.event]);
setAppointments((prevAppointments) => prevAppointments.filter((item) => item.id !== args.event.id));
}, []);
const handleFailed = useCallback((event: MbscCalendarEvent) => {
if (event.start! <= today) {
setToastMessage("Can't add event in the past");
} else {
setToastMessage('Make sure not to double book');
}
setToastOpen(true);
}, []);
const handleEventCreateFailed = useCallback(
(args: MbscEventCreateFailedEvent) => {
handleFailed(args.event);
},
[handleFailed],
);
const handleEventUpdateFailed = useCallback(
(args: MbscEventUpdateFailedEvent) => {
handleFailed(args.event);
},
[handleFailed],
);
const handleEventDelete = useCallback((args: MbscEventDeleteEvent) => {
setToastMessage(args.event.title + ' unscheduled');
setToastOpen(true);
setEvents((prevEvents) => prevEvents.filter((item) => item.id !== args.event.id));
}, []);
const handleEventDragEnter = useCallback(() => {
setColors([
{
background: '#f1fff24d',
start: '08:00',
end: '20:00',
recurring: {
repeat: 'daily',
},
},
]);
}, []);
const handleEventDragLeave = useCallback(() => {
setColors([]);
}, []);
const handleItemDrop = useCallback((args: MbscItemDragEvent) => {
if (args.data) {
args.data.unscheduled = true;
setAppointments((prevAppointments) => [...prevAppointments, args.data]);
}
setContBg('');
}, []);
const handleItemDragEnter = useCallback((args: MbscItemDragEvent) => {
if (!(args.data && args.data.unscheduled)) {
setContBg('#d0e7d2cc');
}
}, []);
const handleItemDragLeave = useCallback(() => {
setContBg('');
}, []);
const handleToastClose = useCallback(() => {
setToastOpen(false);
}, []);
return (
<div className="mbsc-grid mbsc-no-padding">
<div className="mbsc-row">
<div className="mbsc-col-sm-9 docs-appointment-calendar">
<Eventcalendar
data={myEvents}
view={myView}
resources={doctors}
invalid={myInvalid}
dragToMove={true}
dragToCreate={true}
eventOverlap={false}
externalDrop={true}
externalDrag={true}
colors={myColors}
onEventCreate={handleEventCreate}
onEventCreated={handleEventCreated}
onEventCreateFailed={handleEventCreateFailed}
onEventUpdateFailed={handleEventUpdateFailed}
onEventDelete={handleEventDelete}
onEventDragEnter={handleEventDragEnter}
onEventDragLeave={handleEventDragLeave}
/>
<Toast isOpen={isToastOpen} message={toastMessage} onClose={handleToastClose} />
</div>
<div className="mbsc-col-sm-3 docs-appointment-cont" ref={setDropElm} style={{ backgroundColor: contBg }}>
<Dropcontainer
onItemDrop={handleItemDrop}
onItemDragEnter={handleItemDragEnter}
onItemDragLeave={handleItemDragLeave}
element={dropCont}
>
<div className="mbsc-form-group-title">Unscheduled appointments</div>
{appointments.map((app) => (
<Appointment key={app.id} data={app} />
))}
</Dropcontainer>
</div>
</div>
</div>
);
};
export default App;
.docs-appointment-calendar {
border-right: 1px solid #ccc;
}
.docs-appointment-calendar .mbsc-readonly-event {
opacity: 0.6;
}
.docs-appointment-cont {
height: 100%;
overflow: auto;
}
.docs-appointment-task {
color: #fff;
background: #999;
padding: 10px;
margin: 20px;
border-radius: 8px;
font-family:
-apple-system,
Segoe UI,
Roboto,
sans-serif;
}
.demo-doctors-appointment.demo-wrapper,
.demo-doctors-appointment .mbsc-grid,
.demo-doctors-appointment .mbsc-row,
.demo-doctors-appointment .docs-appointment-calendar {
height: 100%;
}