diff --git a/aleksis/core/schema.py b/aleksis/core/schema.py
index a29ef5ee222ed2ad15a2d28da939d9d7752a0bb0..8841d02d3a446cb99ec3a9c4f147e87dd58ae24e 100644
--- a/aleksis/core/schema.py
+++ b/aleksis/core/schema.py
@@ -34,6 +34,22 @@ class PersonMutation(DjangoModelFormMutation):
         form_class = PersonForm
 
 
+class MarkNotificationReadMutation(graphene.Mutation):
+    class Arguments:
+        id = graphene.ID()
+
+    notification = graphene.Field(NotificationType)
+
+    @classmethod
+    def mutate(cls, root, info, id):
+        notification = Notification.objects.get(pk=id)
+        # FIXME permissions
+        notification.read = True
+        notification.save()
+
+        return notification
+
+
 class Query(graphene.ObjectType):
     ping = graphene.String(default_value="pong")
 
@@ -64,6 +80,7 @@ class Query(graphene.ObjectType):
 class Mutation(graphene.ObjectType):
     update_person = PersonMutation.Field()
 
+    mark_notification_read = MarkNotificationReadMutation.Field()
 
 def build_global_schema():
     """Build global GraphQL schema from all apps."""
diff --git a/aleksis/core/urls.py b/aleksis/core/urls.py
index d0243c205c84c61802be00d98594a4966715e190..54e854c8117843e8bd4f5139280bd4c46318a95c 100644
--- a/aleksis/core/urls.py
+++ b/aleksis/core/urls.py
@@ -97,11 +97,6 @@ urlpatterns = [
     path("group/<int:id_>/delete", views.delete_group, name="delete_group_by_id"),
     path("", views.index, name="index"),
     path("dashboard/edit/", views.EditDashboardView.as_view(), name="edit_dashboard"),
-    path(
-        "notifications/mark-read/<int:id_>",
-        views.notification_mark_read,
-        name="notification_mark_read",
-    ),
     path("groups/group_type/create", views.edit_group_type, name="create_group_type"),
     path(
         "groups/group_type/<int:id_>/delete",
diff --git a/aleksis/core/views.py b/aleksis/core/views.py
index 8e2d5ef3327f41f50f042bc5965e50244f75b991..df8469036d2e4330f66e92455f4a21156c4d84ca 100644
--- a/aleksis/core/views.py
+++ b/aleksis/core/views.py
@@ -539,20 +539,6 @@ def vue_dummy(request: HttpRequest) -> HttpResponse:
     # FIXME remove together with URL route and template
     return render(request, "core/vue_dummy.html", {})
 
-@permission_required(
-    "core.mark_notification_as_read_rule", fn=objectgetter_optional(Notification, None, False)
-)
-def notification_mark_read(request: HttpRequest, id_: int) -> HttpResponse:
-    """Mark a notification read."""
-    notification = objectgetter_optional(Notification, None, False)(request, id_)
-
-    notification.read = True
-    notification.save()
-
-    # Redirect to dashboard as this is only used from there if JavaScript is unavailable
-    return redirect("index")
-
-
 @permission_required("core.view_announcements_rule")
 def announcements(request: HttpRequest) -> HttpResponse:
     """List view of announcements."""
diff --git a/assets/components/notifications/NotificationItem.vue b/assets/components/notifications/NotificationItem.vue
index 01e52aa9b4e41194015d242d3b5ea11f0ab60b3f..dea29654a5ddf833ec3433c3f63d4a5d8b0c714d 100644
--- a/assets/components/notifications/NotificationItem.vue
+++ b/assets/components/notifications/NotificationItem.vue
@@ -1,28 +1,46 @@
 <template>
-  <v-list-item>
-    <v-list-item-content>
-      <v-list-item-title>{{ notification.title }}</v-list-item-title>
+  <ApolloMutation
+    :mutation="gql => gql`
+      mutation ($id: ID!) {
+        markNotificationRead(id: $id) {
+          notification {
+            id
+            read
+          }
+        }
+      }
+    `"
+    :variables="{ id: this.notification.id }"
+  >
+    <template v-slot="{ mutate, loading, error }">
+      <v-list-item
+        v-intersect="mutate"
+      >
+        <v-list-item-content>
+          <v-list-item-title>{{ notification.title }}</v-list-item-title>
 
-      <v-list-item-subtitle>
-        <v-icon>mdi-clock-outline</v-icon>
-        {{ notification.created }}
-      </v-list-item-subtitle>
+          <v-list-item-subtitle>
+            <v-icon>mdi-clock-outline</v-icon>
+            {{ notification.created }}
+          </v-list-item-subtitle>
 
-      <v-list-item-subtitle>
-        {{ notification.description }}
-      </v-list-item-subtitle>
-    </v-list-item-content>
+          <v-list-item-subtitle>
+            {{ notification.description }}
+          </v-list-item-subtitle>
+        </v-list-item-content>
 
-    <v-list-item-action v-if="notification.link">
-      <v-btn text :href="notification.link">
-        {{ this.$root.django.gettext('More information →') }}
-      </v-btn>
-    </v-list-item-action>
+        <v-list-item-action v-if="notification.link">
+          <v-btn text :href="notification.link">
+            {{ this.$root.django.gettext('More information →') }}
+          </v-btn>
+        </v-list-item-action>
 
-    <v-list-item-icon>
-      <v-chip color="primary">{{ notification.sender }}</v-chip>
-    </v-list-item-icon>
-  </v-list-item>
+        <v-list-item-icon>
+          <v-chip color="primary">{{ notification.sender }}</v-chip>
+        </v-list-item-icon>
+      </v-list-item>
+    </template>
+  </ApolloMutation>
 </template>
 
 <script>