---
title: "React Calendar Appointment Booking Example | Mobiscroll"
component: "Calendar"
framework: "React"
library_version: "6.1.5"
url: "https://demo.mobiscroll.com/react/calendar/appointment-booking"
---

# Appointment Booking - React Calendar demo

Build booking date selection interfaces with an inline React calendar — show per-day prices and disable fully booked dates for appointment flows. For React and Next.JS.

- [Live demo](https://demo.mobiscroll.com/react/calendar/appointment-booking)
- [View React JS code on GitHub](https://github.com/acidb/mobiscroll-demos-react/tree/main/src/demos/datepicker/calendar/appointment-booking)
- [View React TS code on GitHub](https://github.com/acidb/mobiscroll-demos-react-ts/tree/main/src/demos/datepicker/calendar/appointment-booking)
- This demo in other frameworks: [JavaScript](https://demo.mobiscroll.com/javascript/calendar/appointment-booking.md), [jQuery](https://demo.mobiscroll.com/jquery/calendar/appointment-booking.md), [Angular](https://demo.mobiscroll.com/angular/calendar/appointment-booking.md), [Vue](https://demo.mobiscroll.com/vue/calendar/appointment-booking.md)

## Description

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](https://demo.mobiscroll.com/react/calendar/dots-colors-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.](https://demo.mobiscroll.com/react/calendar/usage-on-input-or-inline)

**Looking for flight or hotel booking?** [Check out the range picker →](https://demo.mobiscroll.com/react/range/flight-booking)

## What this demo shows

- Three inline appointment-booking examples that show different date-selection patterns.
- **Single-date booking** The first example shows an inline calendar with two months visible side by side.
- **Single-date booking** Past dates are disabled, and booking is limited to a six-month window starting from today.
- **Single-date booking** Some dates are unavailable because they are already booked.
- **Single-date booking** Each day shows a price label below the date.
- **Date and time booking** The second example pairs a single-month date picker with a time slot picker shown next to it.
- **Date and time booking** Users can browse dates from today up to six months ahead and then select from the available time slots for the chosen day.
- **Date and time booking** Each day shows how many spots are still available.
- **Date and time booking** Fully booked days are disabled, and taken time slots remain visible but cannot be selected.
- **Multi-date booking** The third example shows an inline two-month calendar with multiple date selection enabled.
- **Multi-date booking** Each day shows the number of available spots, and dates with no remaining availability are disabled.
- **Calendar header** All three examples include month labels and navigation arrows in the calendar header.

## Best for

- **Single-appointment booking** Consumer-facing flows where users need to choose one available date and see day-based pricing.
- **Date and time selection** Booking experiences where users pick a date first and then choose from the remaining time slots for that day.
- **Multi-visit booking** Flows that let people book multiple appointment dates in one interaction.
- **Availability-driven booking** Interfaces that need to show remaining capacity, disable fully booked dates, and surface useful day-level booking details.
- **Common appointment scenarios** Service bookings, home visits, consultations, demos, sales meetings, and similar scheduled appointments.

## Tutorial

### Single date selection

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.

```jsx
const 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}
/>
```

### Booking a date and time

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: '2026-10-29',
        textColor: '#e1528f',
        title: '3 SPOTS'
    }];
}, []);

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}
/>
```

### Booking multiple appointments

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.

```jsx
const 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}
/>
```

## API used in this demo

- [`theme`](https://mobiscroll.com/docs/react/datepicker/api#opt-theme) - option
- [`themeVariant`](https://mobiscroll.com/docs/react/datepicker/api#opt-themeVariant) - option
- [`display`](https://mobiscroll.com/docs/react/datepicker/api#opt-display) - option
- [`controls`](https://mobiscroll.com/docs/react/datepicker/api#opt-controls) - option
- [`min`](https://mobiscroll.com/docs/react/datepicker/api#opt-min) - option
- [`max`](https://mobiscroll.com/docs/react/datepicker/api#opt-max) - option
- [`labels`](https://mobiscroll.com/docs/react/datepicker/api#opt-labels) - option
- [`invalid`](https://mobiscroll.com/docs/react/datepicker/api#opt-invalid) - option
- [`pages`](https://mobiscroll.com/docs/react/datepicker/api#opt-pages) - option
- [`onPageLoading`](https://mobiscroll.com/docs/react/datepicker/api#event-onPageLoading) - event
- [`minTime`](https://mobiscroll.com/docs/react/datepicker/api#opt-minTime) - option
- [`maxTime`](https://mobiscroll.com/docs/react/datepicker/api#opt-maxTime) - option
- [`stepMinute`](https://mobiscroll.com/docs/react/datepicker/api#opt-stepMinute) - option
- [`cssClass`](https://mobiscroll.com/docs/react/datepicker/api#opt-cssClass) - option
- [`selectMultiple`](https://mobiscroll.com/docs/react/datepicker/api#opt-selectMultiple) - option
- [`onChange`](https://mobiscroll.com/docs/react/datepicker/api#event-onChange) - event

## Code

### JSX

```jsx
import { Datepicker, getJson, Page, setOptions } from '@mobiscroll/react';
import { useCallback, useState } from 'react';

setOptions({
  theme: 'ios',
  themeVariant: 'light'
});

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;
```

### TSX

```tsx
import {
  Datepicker,
  getJson,
  MbscCalendarLabel,
  MbscDatepickerChangeEvent,
  MbscDatepickerPageLoadingEvent,
  MbscDateType,
  Page,
  setOptions,
} from '@mobiscroll/react';
import { FC, useCallback, useState } from 'react';

setOptions({
  theme: 'ios',
  themeVariant: 'light'
});

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;
```

### CSS

```css
.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;
}
```

## Implementation notes

- **Booking window** Use `min` and `max` to limit selection to the allowed booking range, such as from today through the next six months.
- **Unavailable dates** Use `invalid` to disable dates that are fully booked or otherwise unavailable.
- **Day labels** Use `labels` to display supporting day-level information such as pricing or remaining spots.
- **Dynamic data loading** Set `invalid` dates and `labels` on load or update them in the `onPageLoading` lifecycle event when availability and pricing need to be loaded for the visible date range.
- **Date and time booking** Combine the calendar with a timegrid for date-and-time selection, then filter the time slots based on availability for the selected day.
- **Multi-date booking** Set `selectMultiple` to `true` when the booking flow needs to allow selecting more than one date.

## Run it in your project

1. If not signed up yet, or don't own a license, you can [start a free trial](https://mobiscroll.com/starttrial).

2. Clone the repository:

   ```
   git clone https://github.com/acidb/mobiscroll-demos-react.git
   ```

3. If not installed yet, install the Mobiscroll CLI from NPM:

   ```
   npm install -g @mobiscroll/cli
   ```

4. Config Mobiscroll - open a terminal window in the root directory and run:

   ```
   mobiscroll config react
   ```

5. Install dependencies - open a terminal window in the root directory and run:

   ```
   npm install
   ```

6. Run the project:

   ```
   npm run dev
   ```

On a trial, add `--trial` to the config command: `mobiscroll config react --trial`.

Running a demo locally requires a Mobiscroll trial or a license; setup is per framework at https://mobiscroll.com/docs/react/getting-started/installation

## Related demos

- Next: [Custom Day Cells](https://demo.mobiscroll.com/react/calendar/activity-calendar.md) - Add custom activity indicators, availability markers, or daily metrics to each day cell in the inline React calendar date picker.
