---
title: "Vue Calendar Appointment Booking Example | Mobiscroll"
component: "Calendar"
framework: "Vue"
library_version: "6.2.0"
url: "https://demo.mobiscroll.com/vue/calendar/appointment-booking"
---

# Appointment Booking - Vue Calendar demo

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

- [Live demo](https://demo.mobiscroll.com/vue/calendar/appointment-booking)
- [View SFC JS code on GitHub](https://github.com/acidb/mobiscroll-demos-vue/tree/main/src/demos/datepicker/calendar/appointment-booking)
- [View SFC TS code on GitHub](https://github.com/acidb/mobiscroll-demos-vue-ts/tree/main/src/demos/datepicker/calendar/appointment-booking)
- This demo in other frameworks: [JavaScript](https://demo.mobiscroll.com/calendar/appointment-booking.md), [jQuery](https://demo.mobiscroll.com/jquery/calendar/appointment-booking.md), [Angular](https://demo.mobiscroll.com/angular/calendar/appointment-booking.md), [React](https://demo.mobiscroll.com/react/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/vue/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/vue/calendar/usage-on-input-or-inline)

**Looking for flight or hotel booking?** [Check out the range picker →](https://demo.mobiscroll.com/vue/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 `@page-loading` lifecycle event.

```vue
<script setup>
const minDate = '2026-11-05T00:00';
const maxDate = '2027-05-05T00: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>
```

### 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.

```vue
<script setup>
const minDate = '2026-11-05T00:00';
const maxDate = '2027-05-05T00: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>
```

### 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.

```vue
<script setup>
const multiple = [
  '2026-11-11T00:00',
  '2026-11-16T00:00',
  '2026-11-17T00:00'
]
const minDate = '2026-11-05T00:00';
const maxDate = '2027-05-05T00: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>
```

## API used in this demo

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

## Code

### SFC JS

```vue
<script setup>
import { getJson, MbscDatepicker, MbscPage, setOptions } from '@mobiscroll/vue'
import { ref } from 'vue'

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

const minDate = '2026-11-05T00:00'
const maxDate = '2027-05-05T00: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>
```

### SFC TS

```vue
<script setup lang="ts">
import { getJson, MbscDatepicker, MbscPage, setOptions } from '@mobiscroll/vue'
import { ref } from 'vue'

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

const minDate = '2026-11-05T00:00'
const maxDate = '2027-05-05T00: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>
```

## 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-vue.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 vue
   ```

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 vue --trial`.

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

## Related demos

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