diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/Coursebook.vue b/aleksis/apps/alsijil/frontend/components/coursebook/Coursebook.vue
index 9168b2e4f41ef56983f64fe41dc28526985ba776..af143a59af433bba0d8ed87fa8beccabbbac6dfe 100644
--- a/aleksis/apps/alsijil/frontend/components/coursebook/Coursebook.vue
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/Coursebook.vue
@@ -6,9 +6,10 @@
     :enable-create="false"
     :enable-edit="false"
     :elevated="false"
-    :items-per-page="-1"
     @lastQuery="lastQuery = $event"
     ref="iterator"
+    fixed-header
+    disable-pagination
     hide-default-footer
     use-deep-search
   >
@@ -16,33 +17,35 @@
       <coursebook-filters v-model="filters" />
     </template>
     <template #default="{ items }">
-      <v-list-item
-        v-for="day in groupDocsByDay(items)"
-        two-line
-        :key="'day-' + day[0]"
-      >
-        <v-list-item-content :id="'documentation_' + day[0].toISODate()">
-          <v-subheader class="text-h6">{{
-            $d(day[0], "dateWithWeekday")
-          }}</v-subheader>
-          <v-list max-width="100%" class="pt-0 mt-n1">
-            <v-list-item
-              v-for="doc in day.slice(1)"
-              :key="'documentation-' + (doc.oldId || doc.id)"
-            >
-              <documentation-modal
-                :documentation="doc"
-                :affected-query="lastQuery"
-              />
-            </v-list-item>
-          </v-list>
-        </v-list-item-content>
-      </v-list-item>
+      <coursebook-loader />
+      <coursebook-day
+        v-for="{ date, docs, first, last } in groupDocsByDay(items)"
+        v-intersect="{
+          handler: intersectHandler(date, first, last),
+          options: {
+            rootMargin: '-' + topMargin + 'px 0px 0px 0px',
+            threshold: [0, 1],
+          },
+        }"
+        :date="date"
+        :docs="docs"
+        :lastQuery="lastQuery"
+        :focus-on-mount="initDate && initDate.toMillis() === date.toMillis()"
+        @init="transition"
+        :key="'day-' + date"
+        ref="days"
+      />
+      <coursebook-loader />
 
-      <date-select-footer :value="date" @click="handleDateMove" />
+      <date-select-footer
+        :value="currentDate"
+        @input="gotoDate"
+        @prev="gotoPrev"
+        @next="gotoNext"
+      />
     </template>
     <template #loading>
-      <CoursebookLoader />
+      <coursebook-loader :number-of-days="10" :number-of-docs="5" />
     </template>
 
     <template #no-data>
@@ -64,8 +67,8 @@
 <script>
 import CRUDIterator from "aleksis.core/components/generic/CRUDIterator.vue";
 import DateSelectFooter from "aleksis.core/components/generic/DateSelectFooter.vue";
-import DocumentationModal from "./documentation/DocumentationModal.vue";
-import { DateTime } from "luxon";
+import CoursebookDay from "./CoursebookDay.vue";
+import { DateTime, Interval } from "luxon";
 import { documentationsForCoursebook } from "./coursebook.graphql";
 import CoursebookFilters from "./CoursebookFilters.vue";
 import CoursebookLoader from "./CoursebookLoader.vue";
@@ -79,7 +82,7 @@ export default {
     CoursebookLoader,
     CRUDIterator,
     DateSelectFooter,
-    DocumentationModal,
+    CoursebookDay,
   },
   props: {
     filterType: {
@@ -96,36 +99,51 @@ export default {
       required: false,
       default: null,
     },
-    // ISODate
-    date: {
-      type: String,
+    /**
+     * Number of consecutive to load at once
+     * This number of days is initially loaded and loaded
+     * incrementally while scrolling.
+     */
+    dayIncrement: {
+      type: Number,
       required: false,
-      default: "",
+      default: 7,
+    },
+    /**
+     * Margin from coursebook list to top of viewport in pixels
+     */
+    topMargin: {
+      type: Number,
+      required: false,
+      default: 165,
     },
   },
   data() {
     return {
       gqlQuery: documentationsForCoursebook,
       lastQuery: null,
+      dateStart: "",
+      dateEnd: "",
       // Placeholder values while query isn't completed yet
       groups: [],
       courses: [],
-      dateStart: null,
-      dateEnd: null,
       incomplete: false,
+      ready: false,
+      initDate: false,
+      currentDate: "",
+      hashUpdater: false,
     };
   },
   computed: {
+    // Assertion: Should only fire on page load or selection change.
+    //            Resets date range.
     gqlQueryArgs() {
       return {
-        // Assure courseId is a number
         own: this.filterType === "all" ? false : true,
-        objId: this.objId ? Number(this.objId) : null,
+        objId: this.objId ? Number(this.objId) : undefined,
         objType: this.objType?.toUpperCase(),
-        dateStart: this.dateStart ?? this.date,
-        dateEnd:
-          this.dateEnd ??
-          DateTime.fromISO(this.date).plus({ weeks: 1 }).toISODate(),
+        dateStart: this.dateStart,
+        dateEnd: this.dateEnd,
         incomplete: !!this.incomplete,
       };
     },
@@ -147,103 +165,196 @@ export default {
           Object.hasOwn(selectedFilters, "objType")
         ) {
           this.$router.push({
-            name: "alsijil.coursebook_by_type_and_date",
+            name: "alsijil.coursebook",
             params: {
               filterType: selectedFilters.filterType
                 ? selectedFilters.filterType
                 : this.filterType,
               objType: selectedFilters.objType,
               objId: selectedFilters.objId,
-              date: this.date,
             },
+            hash: this.$route.hash,
           });
+          // computed should not have side effects
+          // but this was actually done before filters was refactored into
+          // its own component
+          this.resetDate();
+          // might skip query until both set = atomic
         }
       },
     },
   },
   methods: {
-    // => [[dt doc ...] ...]
+    resetDate(toDate) {
+      // Assure current date
+      console.log("Resetting date range", this.$route.hash);
+      this.currentDate = toDate || this.$route.hash?.substring(1);
+      if (!this.currentDate) {
+        console.log("Set default date");
+        this.setDate(DateTime.now().toISODate());
+      }
+
+      const date = DateTime.fromISO(this.currentDate);
+      this.initDate = date;
+      this.dateStart = date.minus({ days: this.dayIncrement }).toISODate();
+      this.dateEnd = date.plus({ days: this.dayIncrement }).toISODate();
+    },
+    transition() {
+      this.initDate = false;
+      this.ready = true;
+    },
     groupDocsByDay(docs) {
-      const byDay = docs.reduce((byDay, doc) => {
+      // => {dt: {date: dt, docs: doc ...} ...}
+      const docsByDay = docs.reduce((byDay, doc) => {
         // This works with dummy. Does actual doc have dateStart instead?
         const day = DateTime.fromISO(doc.datetimeStart).startOf("day");
-        byDay[day] ??= [day];
-        byDay[day].push(doc);
+        byDay[day] ??= { date: day, docs: [] };
+        byDay[day].docs.push(doc);
         return byDay;
       }, {});
-
-      return Object.keys(byDay)
+      // => [{date: dt, docs: doc ..., idx: idx, lastIdx: last-idx} ...]
+      // sorting is necessary since backend can send docs unordered
+      return Object.keys(docsByDay)
         .sort()
-        .map((key) => byDay[key]);
+        .map((key, idx, { length }) => {
+          const day = docsByDay[key];
+          day.first = idx === 0;
+          const lastIdx = length - 1;
+          day.last = idx === lastIdx;
+          return day;
+        });
     },
-    /**
-     * @param {"prev"|"next"} direction
-     */
-    handleDateMove(direction) {
-      const dateStartParsed = DateTime.fromISO(this.dateStart);
-      const dateEndParsed = DateTime.fromISO(this.dateEnd);
-      const dateParsed = DateTime.fromISO(this.date);
-
-      const newDate =
-        direction === "prev"
-          ? dateParsed.minus({ days: 1 })
-          : dateParsed.plus({ days: 1 });
-
-      /*
-       TODO:
-         Everything below this line is also needed for when a date is selected via the calendar.
-         → probably move this into a different function and create a second event listener for the input event.
-       */
-
-      // Load 3 days into the future/past
-      if (dateStartParsed >= newDate) {
-        this.dateStart = newDate.minus({ days: 3 }).toISODate();
-      }
-      if (dateEndParsed <= newDate) {
-        this.dateEnd = newDate.plus({ days: 3 }).toISODate();
-      }
-
-      this.$router.push({
-        name: "alsijil.coursebook_by_type_and_date",
-        params: {
-          filterType: this.filterType,
-          objType: this.objType,
-          objId: this.objId,
-          date: newDate.toISODate(),
+    // docsByDay: {dt: [dt doc ...] ...}
+    fetchMore(from, to, then) {
+      console.log("fetching", from, to);
+      this.lastQuery.fetchMore({
+        variables: {
+          dateStart: from,
+          dateEnd: to,
+        },
+        // Transform the previous result with new data
+        updateQuery: (previousResult, { fetchMoreResult }) => {
+          console.log("Received more");
+          then();
+          return { items: previousResult.items.concat(fetchMoreResult.items) };
         },
       });
-
-      // Define the function to find the nearest ID
-      const ids = Array.from(
-        document.querySelectorAll("[id^='documentation_']"),
-      ).map((el) => el.id);
-
-      // TODO: This should only be done after loading the new data
-      const nearestId = this.findNearestId(newDate, direction, ids);
-      this.$vuetify.goTo("#" + nearestId);
-    },
-    findNearestId(targetDate, direction, ids) {
-      const sortedIds = ids
-        .map((id) => DateTime.fromISO(id.split("_")[1]))
-        .sort((a, b) => a - b);
-
-      if (direction === "prev") {
-        sortedIds.reverse();
+    },
+    setDate(date) {
+      this.currentDate = date;
+      if (!this.hashUpdater) {
+        this.hashUpdater = window.requestIdleCallback(() => {
+          if (!(this.$route.hash.substring(1) === this.currentDate)) {
+            this.$router.replace({ hash: this.currentDate });
+          }
+          this.hashUpdater = false;
+        });
       }
+    },
+    fixScrollPos(height, top) {
+      this.$nextTick(() => {
+        if (height < document.documentElement.scrollHeight) {
+          document.documentElement.scrollTop =
+            document.documentElement.scrollHeight - height + top;
+          this.ready = true;
+        } else {
+          // Update top, could have changed in the meantime.
+          this.fixScrollPos(height, document.documentElement.scrollTop);
+        }
+      });
+    },
+    intersectHandler(date, first, last) {
+      let once = true;
+      return (entries) => {
+        const entry = entries[0];
+        if (entry.isIntersecting) {
+          if (entry.boundingClientRect.top <= this.topMargin || first) {
+            console.log("@ ", date.toISODate());
+            this.setDate(date.toISODate());
+          }
 
-      const nearestId =
-        sortedIds.find((id) =>
-          direction === "next" ? id >= targetDate : id <= targetDate,
-        ) || sortedIds[sortedIds.length - 1];
+          if (once && this.ready && first) {
+            console.log("load up", date.toISODate());
+            this.ready = false;
+            this.fetchMore(
+              date.minus({ days: this.dayIncrement }).toISODate(),
+              date.minus({ days: 1 }).toISODate(),
+              () => {
+                this.fixScrollPos(
+                  document.documentElement.scrollHeight,
+                  document.documentElement.scrollTop,
+                );
+              },
+            );
+            once = false;
+          } else if (once && this.ready && last) {
+            console.log("load down", date.toISODate());
+            this.ready = false;
+            this.fetchMore(
+              date.plus({ days: 1 }).toISODate(),
+              date.plus({ days: this.dayIncrement }).toISODate(),
+              () => {
+                this.ready = true;
+              },
+            );
+            once = false;
+          }
+        }
+      };
+    },
+    // Improve me?
+    // The navigation logic could be a bit simpler if the current days
+    // where known as a sorted array (= result of groupDocsByDay) But
+    // then the list would need its own component and this gets rather
+    // complicated. Then the calendar could also show the present days
+    // / gray out the missing.
+    //
+    // Next two: arg date is ts object
+    findPrev(date) {
+      return this.$refs.days
+        .map((day) => day.date)
+        .sort()
+        .reverse()
+        .find((date2) => date2 < date);
+    },
+    findNext(date) {
+      return this.$refs.days
+        .map((day) => day.date)
+        .sort()
+        .find((date2) => date2 > date);
+    },
+    gotoDate(date) {
+      const present = this.$refs.days.find(
+        (day) => day.date.toISODate() === date,
+      );
 
-      return "documentation_" + nearestId.toISODate();
+      if (present) {
+        // React immediatly -> smoother navigation
+        // Also intersect handler does not always react to scrollIntoView
+        this.setDate(date);
+        present.focus("smooth");
+      } else if (
+        !this.findPrev(DateTime.fromISO(date)) ||
+        !this.findNext(DateTime.fromISO(date))
+      ) {
+        this.resetDate(date);
+      }
+    },
+    gotoPrev() {
+      const prev = this.findPrev(DateTime.fromISO(this.currentDate));
+      if (prev) {
+        this.gotoDate(prev.toISODate());
+      }
+    },
+    gotoNext() {
+      const next = this.findNext(DateTime.fromISO(this.currentDate));
+      if (next) {
+        this.gotoDate(next.toISODate());
+      }
     },
   },
-  mounted() {
-    this.dateStart = this.date;
-    this.dateEnd = DateTime.fromISO(this.dateStart)
-      .plus({ weeks: 1 })
-      .toISODate();
+  created() {
+    this.resetDate();
   },
 };
 </script>
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/CoursebookDay.vue b/aleksis/apps/alsijil/frontend/components/coursebook/CoursebookDay.vue
new file mode 100644
index 0000000000000000000000000000000000000000..14411e9ce9c1c2780402890a80aa907b246262ff
--- /dev/null
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/CoursebookDay.vue
@@ -0,0 +1,66 @@
+<template>
+  <v-list-item :style="{ scrollMarginTop: '145px' }" two-line>
+    <v-list-item-content>
+      <v-subheader class="text-h6">{{
+        $d(date, "dateWithWeekday")
+      }}</v-subheader>
+      <v-list max-width="100%" class="pt-0 mt-n1">
+        <v-list-item
+          v-for="doc in docs"
+          :key="'documentation-' + (doc.oldId || doc.id)"
+        >
+          <documentation-modal
+            :documentation="doc"
+            :affected-query="lastQuery"
+          />
+        </v-list-item>
+      </v-list>
+    </v-list-item-content>
+  </v-list-item>
+</template>
+
+<script>
+import DocumentationModal from "./documentation/DocumentationModal.vue";
+export default {
+  name: "CoursebookDay",
+  components: {
+    DocumentationModal,
+  },
+  props: {
+    date: {
+      type: Object,
+      required: true,
+    },
+    docs: {
+      type: Array,
+      required: true,
+    },
+    lastQuery: {
+      type: Object,
+      required: true,
+    },
+    focusOnMount: {
+      type: Boolean,
+      required: false,
+      default: false,
+    },
+  },
+  emits: ["init"],
+  methods: {
+    focus(how) {
+      this.$el.scrollIntoView({
+        behavior: how,
+        block: "start",
+        inline: "nearest",
+      });
+      console.log("focused @", this.date.toISODate());
+    },
+  },
+  mounted() {
+    if (this.focusOnMount) {
+      this.$nextTick(this.focus("instant"));
+      this.$emit("init");
+    }
+  },
+};
+</script>
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/CoursebookLoader.vue b/aleksis/apps/alsijil/frontend/components/coursebook/CoursebookLoader.vue
index a99df0588b779a7643b5ae66617132110c49b67c..a5136eb9f0c2535009fd78a02e7481aebacd12b4 100644
--- a/aleksis/apps/alsijil/frontend/components/coursebook/CoursebookLoader.vue
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/CoursebookLoader.vue
@@ -1,12 +1,12 @@
 <template>
   <div>
-    <v-list-item v-for="i in 10" :key="'i-' + i">
+    <v-list-item v-for="i in numberOfDays" :key="'i-' + i">
       <v-list-item-content>
         <v-list-item-title>
           <v-skeleton-loader type="heading" />
         </v-list-item-title>
         <v-list max-width="100%">
-          <v-list-item v-for="j in 5" :key="'j-' + j">
+          <v-list-item v-for="j in numberOfDocs" :key="'j-' + j">
             <DocumentationLoader />
           </v-list-item>
         </v-list>
@@ -20,5 +20,17 @@ import DocumentationLoader from "./documentation/DocumentationLoader.vue";
 export default {
   name: "CoursebookLoader",
   components: { DocumentationLoader },
+  props: {
+    numberOfDays: {
+      type: Number,
+      required: false,
+      default: 1,
+    },
+    numberOfDocs: {
+      type: Number,
+      required: false,
+      default: 1,
+    },
+  },
 };
 </script>
diff --git a/aleksis/apps/alsijil/frontend/index.js b/aleksis/apps/alsijil/frontend/index.js
index 6cfb71044ee40390b0ae99509b8ec7a2b6eba0fa..1ec79280e06ef377c4be215c27b5291c51043522 100644
--- a/aleksis/apps/alsijil/frontend/index.js
+++ b/aleksis/apps/alsijil/frontend/index.js
@@ -60,14 +60,14 @@ export default {
       component: () => import("./components/coursebook/Coursebook.vue"),
       redirect: () => {
         return {
-          name: "alsijil.coursebook_by_type_and_date",
+          name: "alsijil.coursebook",
           params: {
-            date: DateTime.now().toISODate(),
             filterType: "my",
           },
+          hash: "#" + DateTime.now().toISODate(),
         };
       },
-      name: "alsijil.coursebook",
+      name: "alsijil.coursebook_landing",
       props: true,
       meta: {
         inMenu: true,
@@ -79,9 +79,9 @@ export default {
       },
       children: [
         {
-          path: ":date(\\d\\d\\d\\d-\\d\\d-\\d\\d)/:filterType(my|all)/:objType(group|course|teacher)?/:objId(\\d+)?/",
+          path: ":filterType(my|all)/:objType(group|course|teacher)?/:objId(\\d+)?/",
           component: () => import("./components/coursebook/Coursebook.vue"),
-          name: "alsijil.coursebook_by_type_and_date",
+          name: "alsijil.coursebook",
           meta: {
             titleKey: "alsijil.coursebook.menu_title",
             toolbarTitle: "alsijil.coursebook.menu_title",