diff --git a/src/Pages/EmissionManager.vue b/src/Pages/EmissionManager.vue
index c1c33edff5e9cfed673c19028dd89015bc3d5141..0edb78e67276097bae3fd17ce9a0ccc637a05347 100644
--- a/src/Pages/EmissionManager.vue
+++ b/src/Pages/EmissionManager.vue
@@ -211,6 +211,8 @@
 </template>
 
 <script>
+import { parseISO } from 'date-fns'
+import { h } from 'vue'
 import { mapGetters } from 'vuex'
 
 import FullCalendar from '@fullcalendar/vue3'
@@ -227,7 +229,12 @@ import playlist from '@/mixins/playlist'
 import ServerErrors from '@/components/ServerErrors.vue'
 import { getISODateString } from '@/utilities'
 import PageHeader from '@/components/PageHeader.vue'
-import { getClosestSlot, getNextAvailableSlot, sanitizeHTML } from '@/util'
+import {
+  calculateDurationSeconds,
+  getClosestSlot,
+  getNextAvailableSlot,
+  sanitizeHTML,
+} from '@/util'
 import SafeHTML from '@/components/generic/SafeHTML'
 
 export default {
@@ -341,6 +348,37 @@ export default {
         allDaySlot: false,
         editable: false,
         nowIndicator: true,
+        eventContent({ event, timeText }) {
+          // The eventContent function doesn’t quite seem to work like documented in the fullcalendar docs:
+          // * the slot is broken because it doesn’t receive a context object ('arg' is undefined)
+          // * the Preact createElement/h function that is passed as the second argument to this function
+          //   doesn’t render anything
+          // * returning { html: '<i>hello</i>' } doesn’t render anything
+          // * returning { domNodes: [(() => { const i = document.createElement('i'); i.textContent = 'hello'; return i })()] }
+          //   doesn’t render anything
+          //
+          // Instead, this comment [1] mentions that one should use the Vue createElement/h function.
+          // Surprisingly this works.
+          //
+          // It is unclear to me why all these (documented) options fail. One major difference is that we run Vue 3 in
+          // Vue 2 compat mode which might be a source of errors.
+          //
+          // [1]: https://github.com/fullcalendar/fullcalendar/issues/7175#issuecomment-1409519357
+
+          const { durationMinutes, title } = event.extendedProps
+          // don’t render any content if it would be crammed anyway
+          const content =
+            durationMinutes > slotDurationMinutes
+              ? [
+                  h('div', { class: 'fc-event-time' }, timeText),
+                  h('div', { class: 'fc-event-title-container' }, [
+                    h('div', { class: 'fc-event-title fc-sticky' }, title),
+                  ]),
+                ]
+              : []
+
+          return h('div', { class: 'fc-event-main-frame' }, content)
+        },
         datesSet: (view) => {
           if (
             this.currentStart?.toISOString?.() !== view.start.toISOString() ||
@@ -802,6 +840,8 @@ export default {
             start: timeslot.start,
             end: timeslot.end,
             title,
+            durationMinutes:
+              calculateDurationSeconds(parseISO(timeslot.start), parseISO(timeslot.end)) / 60,
           },
         })
       }
diff --git a/src/util.ts b/src/util.ts
index bdd7077e84f0a125d34676f233dd43b6aab7cbc6..1ff59873167dfaba72739d34c6c66f62bca39799 100644
--- a/src/util.ts
+++ b/src/util.ts
@@ -75,6 +75,10 @@ export function getNextAvailableSlot(slotDurationMinutes: number, date?: Date):
   return new Date(nextSlotTimestamp)
 }
 
+export function calculateDurationSeconds(start: Date, end: Date): number {
+  return Math.abs(end.getTime() - start.getTime()) / 1000
+}
+
 export function sanitizeHTML(
   html: string,
   preset?: 'inline-noninteractive' | 'safe-html' | 'strip',