diff --git a/app/views/consultation/admin/create.php b/app/views/consultation/admin/create.php
index aa4ee30636ae46f58518e8cc559bfe06929192ee..ff0b4831d4c982fff7b10563b3bc30aa9700d801 100644
--- a/app/views/consultation/admin/create.php
+++ b/app/views/consultation/admin/create.php
@@ -61,7 +61,7 @@ $intervals = [
             <input required type="text" name="start-date" id="start-date"
                    value="<?= htmlReady(Request::get('start-date', strftime('%d.%m.%Y', strtotime('+7 days'))))  ?>"
                    placeholder="<?= _('tt.mm.jjjj') ?>"
-                   data-date-picker='{">=":"today"}'>
+                   data-date-picker='{">=":"today","disable_holidays": true}'>
         </label>
 
         <label class="col-3">
@@ -70,7 +70,7 @@ $intervals = [
             <input required type="text" name="end-date" id="end-date"
                    value="<?= htmlReady(Request::get('end-date', strftime('%d.%m.%Y', strtotime('+4 weeks'))))  ?>"
                    placeholder="<?= _('tt.mm.jjjj') ?>"
-                   data-date-picker='{">=":"#start-date"}'>
+                   data-date-picker='{">=":"#start-date","disable_holidays": true}'>
         </label>
 
         <label class="col-3">
diff --git a/lib/classes/JsonApi/RouteMap.php b/lib/classes/JsonApi/RouteMap.php
index 0709ef3a75d82e7c760d22c5b2b59e1b6c9bac92..bffa13a59bdd8e53c1b627e0d457bdffb3bd6e2d 100644
--- a/lib/classes/JsonApi/RouteMap.php
+++ b/lib/classes/JsonApi/RouteMap.php
@@ -7,6 +7,7 @@ use JsonApi\Middlewares\Authentication;
 use JsonApi\Middlewares\DangerousRouteHandler;
 use JsonApi\Middlewares\JsonApi as JsonApiMiddleware;
 use JsonApi\Middlewares\StudipMockNavigation;
+use JsonApi\Routes\Holidays\HolidaysShow;
 use Slim\Routing\RouteCollectorProxy;
 
 /**
@@ -143,6 +144,8 @@ class RouteMap
     {
         \PluginEngine::sendMessage(JsonApiPlugin::class, 'registerUnauthenticatedRoutes', $group);
 
+        $group->get('/holidays', HolidaysShow::class);
+
         $group->get('/semesters', Routes\SemestersIndex::class);
         $group->get('/semesters/{id}', Routes\SemestersShow::class)->setName('get-semester');
 
@@ -561,4 +564,3 @@ class RouteMap
         $group->map(['GET', 'PATCH', 'POST', 'DELETE'], $url, $handler);
     }
 }
-
diff --git a/lib/classes/JsonApi/Routes/Holidays/HolidaysShow.php b/lib/classes/JsonApi/Routes/Holidays/HolidaysShow.php
new file mode 100644
index 0000000000000000000000000000000000000000..ef3fa61a14b6e7ff334bd9ac10a99fda58f0c04a
--- /dev/null
+++ b/lib/classes/JsonApi/Routes/Holidays/HolidaysShow.php
@@ -0,0 +1,172 @@
+<?php
+
+namespace JsonApi\Routes\Holidays;
+
+use GuzzleHttp\Psr7;
+use JsonApi\JsonApiIntegration\QueryParserInterface;
+use JsonApi\NonJsonApiController;
+use Neomerx\JsonApi\Exceptions\JsonApiException;
+use Neomerx\JsonApi\Schema\Error;
+use Neomerx\JsonApi\Schema\ErrorCollection;
+use Psr\Container\ContainerInterface;
+use Psr\Http\Message\ServerRequestInterface as Request;
+use Psr\Http\Message\ResponseInterface as Response;
+
+/**
+ * List all holidays for a specific time period.
+ *
+ * Filter are allowed for year, month and days. You must specify a year in order
+ * to filter by a month and you must specify a month in order to filter by a
+ * day.
+ *
+ * If no filter is set, a filter the current year is assumed.
+ */
+final class HolidaysShow extends NonJsonApiController
+{
+    private $query_parser;
+
+    protected $allowedFilteringParameters = ['year', 'month', 'day'];
+
+    public function __construct(
+        ContainerInterface $container,
+        QueryParserInterface $queryParser
+    ) {
+        parent::__construct($container);
+
+        $this->query_parser = $queryParser;
+    }
+
+    public function __invoke(Request $request, Response $response, $args): Response
+    {
+        [$current, $end] = $this->getTimespanByFilters();
+
+        $holidays = [];
+        while ($current < $end) {
+            $holiday = holiday($current);
+            if ($holiday) {
+                $holidays[date('Y-m-d', $current)] = [
+                    'holiday'   => $holiday['name'],
+                    'mandatory' => $holiday['col'] === 3,
+                ];
+            }
+
+            $current = strtotime('+1 day', $current);
+        }
+
+        return $response
+            ->withHeader('Content-Type', 'application/json')
+            ->withBody(Psr7\Utils::streamFor(json_encode($holidays)));
+    }
+
+    private function getTimespanByFilters(): array
+    {
+        $filters = $this->getFilters();
+
+        $begin = mktime(
+            0,
+            0,
+            0,
+            $filters['month'] ?? 1,
+            $filters['day'] ?? 1,
+            $filters['year']
+        );
+
+        $end = mktime(
+            23,
+            59,
+            59,
+            $filters['month'] ?? 12,
+            $filters['day'] ?? $this->getLastDayOfMonth($filters['year'], $filters['month'] ?? 12),
+            $filters['year']
+        );
+
+        return [$begin, $end];
+    }
+
+    private function getLastDayOfMonth(int $year, int $month): int
+    {
+        $first_of_month = mktime(0, 0, 0, $month, 1, $year);
+        $last_day_of_month = strtotime('last day of this month', $first_of_month);
+        return (int) date('d', $last_day_of_month);
+    }
+
+    /**
+     * @todo imporove error handling
+     * @return array
+     */
+    private function getFilters(): array
+    {
+        $errors = new ErrorCollection();
+
+        // Get filters
+        $filters = $this->query_parser->getFilteringParameters();
+
+        // Validate allowed filters
+        foreach ($filters as $key => $value) {
+            if (!in_array($key, $this->allowedFilteringParameters)) {
+                $errors->add(new Error(
+                    'invalid-filter-field',
+                    null, null,
+                    null, null,
+                    'Filter should contain only allowed values.',
+                    "Cannot filter by {$key}",
+                    ['filter' => $key]
+                ));
+            }
+        }
+
+        // Validate month
+        if (isset($filters['month']) && !isset($filters['year'])) {
+            $errors->add(new Error(
+                'missing-year-filter',
+                null, null,
+                null, null,
+                'You must not define a month filter without a year filter'
+            ));
+        } elseif (
+            isset($filters['month'])
+            && ($filters['month'] < 1 || $filters['month'] > 12)
+        ) {
+            $errors->add(new Error(
+                'invalid-filter-value',
+                null, null,
+                null, null,
+                'Filter should contain only allowed values.',
+                "Invalid value {$filters['month']} for month filter",
+                ['filter' => 'month', 'value' => $filters['month']]
+            ));
+        }
+
+        // Validate day
+        if (isset($filters['day']) && !isset($filters['month'])) {
+            $errors->add(new Error(
+                'missing-month-filter',
+                null, null,
+                null, null,
+                'You must not define a day filter without a month filter'
+            ));
+        } elseif (
+            isset($filters['day'])
+            && (
+                $filters['day'] < 1
+                || $filters['day'] > $this->getLastDayOfMonth((int) $filters['year'] ?? date('Y'), $filters['month'])
+            )
+        ) {
+            $errors->add(new Error(
+                'invalid-filter-value',
+                null, null,
+                null, null,
+                'Filter should contain only allowed values.',
+                "Invalid value {$filters['day']} for day filter",
+                ['filter' => 'day', 'value' => $filters['day']]
+            ));
+        }
+
+        if ($errors->count() > 0) {
+            throw new JsonApiException($errors, JsonApiException::HTTP_CODE_BAD_REQUEST);
+        }
+
+        // Apply defaults
+        return array_merge(['year' => date('Y')], $filters);
+    }
+}
diff --git a/resources/assets/javascripts/studip-ui.js b/resources/assets/javascripts/studip-ui.js
index 41897b204b94e5b3d17c2e88b15dbde60fae38f6..63766c08d1b3477f4a88aeb6decf051444aac82b 100644
--- a/resources/assets/javascripts/studip-ui.js
+++ b/resources/assets/javascripts/studip-ui.js
@@ -27,6 +27,37 @@ import eventBus from "./lib/event-bus.ts";
         return element?.classList?.contains('ck-body-wrapper');
     }
 
+    function disableHolidaysBeforeShow(date) {
+        const year = date.getFullYear();
+
+        if (STUDIP.UI.restrictedDates[year] === undefined) {
+            STUDIP.UI.restrictedDates[year] = {};
+
+            STUDIP.jsonapi.GET('holidays', {data: {
+                'filter[year]': year
+            }}).done(response => {
+                // Since PHP will return an empty object as an array,
+                // we need to check
+                if (Array.isArray(response)) {
+                    return;
+                }
+
+                for (const [date, data] of Object.entries(response)) {
+                    STUDIP.UI.addRestrictedDate(
+                        new Date(date),
+                        data.holiday,
+                        data.mandatory
+                    );
+                }
+
+                $(this).datepicker('refresh');
+            });
+        }
+
+        const {reason, lock} = STUDIP.UI.isDateRestricted(date, false);
+        return [!lock, lock ? 'ui-datepicker-is-locked' : null, reason];
+    }
+
     /**
      * Setup and refine date picker, add automated handling for .has-date-picker
      * and [data-date-picker].
@@ -53,27 +84,84 @@ import eventBus from "./lib/event-bus.ts";
     }
 
     // Setup Stud.IP's own datepicker extensions
-    STUDIP.UI = STUDIP.UI || {};
+    STUDIP.UI = Object.assign(STUDIP.UI || {}, {
+        restrictedDates: {},
+        addRestrictedDate(date, reason = '', lock = true) {
+            if (this.isDateRestricted(date)) {
+                return;
+            }
+
+            const [year, month, day] = this.convertDateForRestriction(date);
+            if (this.restrictedDates[year] === undefined) {
+                this.restrictedDates[year] = {};
+            }
+            if (this.restrictedDates[year][month] === undefined) {
+                this.restrictedDates[year][month] = {};
+            }
+
+            this.restrictedDates[year][month][day] = {reason, lock};
+        },
+        removeRestrictedDate(date) {
+            if (!this.isDateRestricted(date)) {
+                return false;
+            }
+            const [year, month, day] = this.convertDateForRestriction(date);
+
+            delete this.restrictedDates[year][month][day];
+
+            if (Object.keys(this.restrictedDates[year][month]).length === 0) {
+                delete this.restrictedDates[year][month];
+            }
+
+            return true;
+        },
+        isDateRestricted(date, return_bool = true) {
+            const [year, month, day] = this.convertDateForRestriction(date);
+            if (
+                this.restrictedDates[year] === undefined
+                || this.restrictedDates[year][month] === undefined
+                || this.restrictedDates[year][month][day] === undefined
+            ) {
+                return return_bool ? false : {
+                    reason: null,
+                    lock: false,
+                };
+            }
+
+            return return_bool ? true : this.restrictedDates[year][month][day];
+        },
+        convertDateForRestriction(date) {
+            return [date.getFullYear(), date.getMonth() + 1, date.getDate()];
+        }
+    });
     STUDIP.UI.Datepicker = {
         selector: '.has-date-picker,[data-date-picker]',
         // Initialize all datepickers that not yet been initialized (e.g. in dialogs)
-        init: function () {
+        init() {
             $(this.selector).filter(function () {
                 return $(this).data('date-picker-init') === undefined;
             }).each(function () {
-                $(this).data('date-picker-init', true).datepicker();
+                const dataOptions = $(this).data().datePicker;
+
+                const options = {};
+                if (
+                    dataOptions['disable_holidays'] !== undefined
+                    && dataOptions['disable_holidays'] === true
+                ) {
+                    options.beforeShowDay = disableHolidaysBeforeShow;
+                }
+                $(this).data('date-picker-init', true).datepicker(options);
             });
         },
         // Apply registered handlers. Take care: This happens upon before a
         // picker is shown as well as after a date has been selected.
-        refresh: function () {
+        refresh() {
             $(this.selector).each(function () {
-                var element = this,
-                    options = $(element).data().datePicker;
+                const options = $(this).data().datePicker;
                 if (options) {
-                    $.each(options, function (key, value) {
+                    $.each(options, (key, value) => {
                         if (STUDIP.UI.Datepicker.dataHandlers[key] !== undefined) {
-                            STUDIP.UI.Datepicker.dataHandlers[key].call(element, value);
+                            STUDIP.UI.Datepicker.dataHandlers[key].call(this, value);
                         }
                     });
                 }
@@ -190,23 +278,31 @@ import eventBus from "./lib/event-bus.ts";
     STUDIP.UI.DateTimepicker = {
         selector: '.has-datetime-picker,[data-datetime-picker]',
         // Initialize all datetimepickers that not yet been initialized (e.g. in dialogs)
-        init: function () {
+        init() {
             $(this.selector).filter(function () {
                 return $(this).data('datetime-picker-init') === undefined;
             }).each(function () {
-                $(this).data('datetime-picker-init', true).datetimepicker();
+                const dataOptions = $(this).data().datePicker;
+
+                const options = {};
+                if (
+                    dataOptions['disable_holidays'] !== undefined
+                    && dataOptions['disable_holidays'] === true
+                ) {
+                    options.beforeShowDay = disableHolidaysBeforeShow;
+                }
+                $(this).data('date-picker-init', true).datepicker(options);
             });
         },
         // Apply registered handlers. Take care: This happens upon before a
         // picker is shown as well as after a date has been selected.
-        refresh: function () {
+        refresh() {
             $(this.selector).each(function () {
-                var element = this,
-                    options = $(element).data().datetimePicker;
+                const options = $(this).data().datetimePicker;
                 if (options) {
-                    $.each(options, function (key, value) {
+                    $.each(options, (key, value) => {
                         if (STUDIP.UI.DateTimepicker.dataHandlers[key] !== undefined) {
-                            STUDIP.UI.DateTimepicker.dataHandlers[key].call(element, value);
+                            STUDIP.UI.DateTimepicker.dataHandlers[key].call(this, value);
                         }
                     });
                 }
@@ -317,7 +413,7 @@ import eventBus from "./lib/event-bus.ts";
     STUDIP.UI.Timepicker = {
         selector: '.has-time-picker,[data-time-picker]',
         // Initialize all datetimepickers that not yet been initialized (e.g. in dialogs)
-        init: function () {
+        init() {
             $(this.selector).filter(function () {
                 return $(this).data('time-picker-init') === undefined;
             }).each(function () {
@@ -326,7 +422,7 @@ import eventBus from "./lib/event-bus.ts";
         },
         // Apply registered handlers. Take care: This happens upon before a
         // picker is shown as well as after a date has been selected.
-        refresh: function () {
+        refresh() {
             $(this.selector).each(function () {
                 var element = this,
                     options = $(element).data().timePicker;
@@ -395,7 +491,6 @@ import eventBus from "./lib/event-bus.ts";
                     parsed.minute
                 );
 
-                console.log('max time:', this_time, max_time);
                 if (this_time && this_time > max_time) {
                     $(this).timepicker(STUDIP.UI.Timepicker.parseTime(max_time));
                 }
@@ -446,7 +541,6 @@ import eventBus from "./lib/event-bus.ts";
                     parsed.minute
                 );
 
-                console.log('min time:', this_time, min_time);
                 if (this_time && this_time < min_time) {
                     $(this).timepicker(STUDIP.UI.Timepicker.parseTime(min_time));
                 }
diff --git a/resources/assets/stylesheets/studip-jquery-ui.less b/resources/assets/stylesheets/studip-jquery-ui.less
index c9fb6defef61ab9df35995996b3301165e4711bb..2303779361dbbd6d8c0b0f1684e6bda6ecd7b624 100644
--- a/resources/assets/stylesheets/studip-jquery-ui.less
+++ b/resources/assets/stylesheets/studip-jquery-ui.less
@@ -176,3 +176,10 @@
 .ui-menu .ui-menu-item {
     list-style: none;
 }
+
+.ui-datepicker-calendar {
+    // This will reenable the tooltip
+    .ui-datepicker-unselectable.ui-datepicker-is-locked {
+        pointer-events: all;
+    }
+}