diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/Coursebook.vue b/aleksis/apps/alsijil/frontend/components/coursebook/Coursebook.vue
index 366681af084c8fb0011f9ecd8c9b4b8182aeb617..f26ebf0b97864eba43ac2cd583b7990921cb09e8 100644
--- a/aleksis/apps/alsijil/frontend/components/coursebook/Coursebook.vue
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/Coursebook.vue
@@ -13,48 +13,7 @@
     use-deep-search
   >
     <template #additionalActions="{ attrs, on }">
-      <div class="d-flex flex-grow-1 justify-end">
-        <v-autocomplete
-          :items="selectable"
-          item-text="name"
-          clearable
-          return-object
-          filled
-          dense
-          hide-details
-          :placeholder="$t('alsijil.coursebook.filter.filter_for_obj')"
-          :loading="selectLoading"
-          :value="currentObj"
-          @input="changeSelection"
-          @click:clear="changeSelection"
-          class="max-width"
-        />
-        <div class="ml-6">
-          <v-switch
-            :loading="selectLoading"
-            :label="$t('alsijil.coursebook.filter.own')"
-            :input-value="filterType === 'my'"
-            @change="
-              changeSelection({
-                filterType: $event ? 'my' : 'all',
-                type: objType,
-                id: objId,
-              })
-            "
-            dense
-            inset
-            hide-details
-          />
-          <v-switch
-            :loading="selectLoading"
-            :label="$t('alsijil.coursebook.filter.missing')"
-            v-model="incomplete"
-            dense
-            inset
-            hide-details
-          />
-        </div>
-      </div>
+      <coursebook-filters v-model="filters" />
     </template>
     <template #default="{ items }">
       <v-list-item
@@ -107,11 +66,8 @@ 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 {
-  coursesOfPerson,
-  documentationsForCoursebook,
-  groupsByPerson,
-} from "./coursebook.graphql";
+import { documentationsForCoursebook } from "./coursebook.graphql";
+import CoursebookFilters from "./CoursebookFilters.vue";
 import CoursebookLoader from "./CoursebookLoader.vue";
 import CoursebookEmptyMessage from "./CoursebookEmptyMessage.vue";
 
@@ -119,6 +75,7 @@ export default {
   name: "Coursebook",
   components: {
     CoursebookEmptyMessage,
+    CoursebookFilters,
     CoursebookLoader,
     CRUDIterator,
     DateSelectFooter,
@@ -158,14 +115,6 @@ export default {
       incomplete: false,
     };
   },
-  apollo: {
-    groups: {
-      query: groupsByPerson,
-    },
-    courses: {
-      query: coursesOfPerson,
-    },
-  },
   computed: {
     gqlQueryArgs() {
       return {
@@ -180,40 +129,30 @@ export default {
         incomplete: !!this.incomplete,
       };
     },
-    selectable() {
-      return [
-        { header: this.$t("alsijil.coursebook.filter.groups") },
-        ...this.groups.map((group) => ({ type: "group", ...group })),
-        { header: this.$t("alsijil.coursebook.filter.courses") },
-        ...this.courses.map((course) => ({ type: "course", ...course })),
-      ];
-    },
-    currentObj() {
-      return this.selectable.find(
-        (o) => o.type === this.objType && o.id === this.objId,
-      );
-    },
-    selectLoading() {
-      return (
-        this.$apollo.queries.groups.loading ||
-        this.$apollo.queries.courses.loading
-      );
-    },
+    filters: {
+      get() {
+        return { objType: this.objType, objId: this.objId, filterType: this.filterType, incomplete: this.incomplete };
+      },
+      set(selectedFilters) {
+        if (Object.hasOwn(selectedFilters, "incomplete")) {
+          this.incomplete = selectedFilters.incomplete;
+        } else if (Object.hasOwn(selectedFilters, "filterType") || Object.hasOwn(selectedFilters, "objId") || Object.hasOwn(selectedFilters, "objType")) {
+          this.$router.push({
+            name: "alsijil.coursebook_by_type_and_date",
+            params: {
+              filterType: selectedFilters.filterType
+                ? selectedFilters.filterType
+                : this.filterType,
+              objType: selectedFilters.objType,
+              objId: selectedFilters.objId,
+              date: this.date,
+            },
+          });
+        }
+      }
+    }
   },
   methods: {
-    changeSelection(selection) {
-      this.$router.push({
-        name: "alsijil.coursebook_by_type_and_date",
-        params: {
-          filterType: selection.filterType
-            ? selection.filterType
-            : this.filterType,
-          objType: selection.type,
-          objId: selection.id,
-          date: this.date,
-        },
-      });
-    },
     // => [[dt doc ...] ...]
     groupDocsByDay(docs) {
       const byDay = docs.reduce((byDay, doc) => {
diff --git a/aleksis/apps/alsijil/frontend/components/coursebook/CoursebookFilters.vue b/aleksis/apps/alsijil/frontend/components/coursebook/CoursebookFilters.vue
new file mode 100644
index 0000000000000000000000000000000000000000..909cf3a20ade0b8aac02241e50be7ff462acd239
--- /dev/null
+++ b/aleksis/apps/alsijil/frontend/components/coursebook/CoursebookFilters.vue
@@ -0,0 +1,105 @@
+<template>
+ <div class="d-flex flex-grow-1 justify-end">
+    <v-autocomplete
+      :items="selectable"
+      item-text="name"
+      clearable
+      return-object
+      filled
+      dense
+      hide-details
+      :placeholder="$t('alsijil.coursebook.filter.filter_for_obj')"
+      :loading="selectLoading"
+      :value="currentObj"
+      @input="selectObject"
+      @click:clear="selectObject"
+      class="max-width"
+    />
+    <div class="ml-6">
+      <v-switch
+        :loading="selectLoading"
+        :label="$t('alsijil.coursebook.filter.own')"
+        :input-value="value.filterType === 'my'"
+        @change="selectFilterType($event)"
+        dense
+        inset
+        hide-details
+      />
+      <v-switch
+        :loading="selectLoading"
+        :label="$t('alsijil.coursebook.filter.missing')"
+        :input-value="value.incomplete"
+        @change="
+          $emit('input', {
+            incomplete: $event
+          })
+        "
+        dense
+        inset
+        hide-details
+      />
+    </div>
+  </div>
+</template>
+
+<script>
+import {
+  coursesOfPerson,
+  groupsByPerson,
+} from "./coursebook.graphql";
+
+export default {
+  name: "CoursebookFilters",
+  data() {
+    return {
+      // Placeholder values while query isn't completed yet
+      groups: [],
+      courses: [],
+    };
+  },
+  props: {
+    value: {
+      type: Object,
+      required: true,
+    },
+  },
+  emits: ["input"],
+  apollo: {
+    groups: {
+      query: groupsByPerson,
+    },
+    courses: {
+      query: coursesOfPerson,
+    },
+  },
+  computed: {
+    selectable() {
+      return [
+        { header: this.$t("alsijil.coursebook.filter.groups") },
+        ...this.groups.map((group) => ({ type: "group", ...group })),
+        { header: this.$t("alsijil.coursebook.filter.courses") },
+        ...this.courses.map((course) => ({ type: "course", ...course })),
+        ];
+    },
+    selectLoading() {
+      return (
+        this.$apollo.queries.groups.loading ||
+        this.$apollo.queries.courses.loading
+      );
+    },
+    currentObj() {
+      return this.selectable.find(
+        (o) => o.type === this.value.objType && o.id === this.value.objId,
+      );
+    },
+  },
+  methods: {
+    selectObject(selection) {
+      this.$emit("input", { objType: selection ? selection.type : null, objId: selection ? selection.id : null })
+    },
+    selectFilterType(switchValue) {
+      this.$emit("input", { filterType: switchValue ? "my" : "all", objType: this.value.objType, objId: this.value.objId });
+    }
+  },
+};
+</script>