diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 75e69afe5c732ba25ff633fd52f93898bc0f0688..4f114a3f40d589bb08e0dae260833e2c1ac3b4da 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -13,6 +13,8 @@ Added
 ~~~~~
 
 * GraphQL schema for Rooms
+* [Dev] UpdateIndicator Vue Component to display the status of interactive pages
+
 
 Fixed
 ~~~~~
diff --git a/aleksis/core/frontend/components/generic/UpdateIndicator.vue b/aleksis/core/frontend/components/generic/UpdateIndicator.vue
new file mode 100644
index 0000000000000000000000000000000000000000..0d23a2cab16a1135d2207aea074b4832520005eb
--- /dev/null
+++ b/aleksis/core/frontend/components/generic/UpdateIndicator.vue
@@ -0,0 +1,85 @@
+<template>
+  <v-tooltip bottom>
+    <template #activator="{ on, attrs }">
+      <v-btn
+        right
+        icon
+        v-bind="attrs"
+        v-on="on"
+        @click="handleClick"
+        :loading="status === $options.UPDATING"
+      >
+        <v-icon v-if="status !== $options.UPDATING" :color="color">
+          {{ icon }}
+        </v-icon>
+      </v-btn>
+    </template>
+    <span>{{ text }}</span>
+  </v-tooltip>
+</template>
+
+<script>
+export default {
+  ERROR: "ERROR", // Something went wrong
+  SAVED: "SAVED", // Everything alright
+  UPDATING: "UPDATING", // We are sending something to the server
+  CHANGES: "CHANGES", // the user changed something, but it has not been saved yet
+  name: "UpdateIndicator",
+  emits: ["manual-update"],
+  props: {
+    status: {
+      type: String,
+      required: true,
+    },
+  },
+  computed: {
+    text() {
+      switch (this.status) {
+        case this.$options.SAVED:
+          return this.$t("status.saved");
+        case this.$options.UPDATING:
+          return this.$t("status.updating");
+        case this.$options.CHANGES:
+          return this.$t("status.changes");
+        default:
+          return this.$t("status.error");
+      }
+    },
+    color() {
+      switch (this.status) {
+        case this.$options.SAVED:
+          return "success";
+        case this.$options.CHANGES:
+          return "secondary";
+        case this.$options.UPDATING:
+          return "secondary";
+        default:
+          return "error";
+      }
+    },
+    icon() {
+      switch (this.status) {
+        case this.$options.SAVED:
+          return "$success";
+        case this.$options.CHANGES:
+          return "mdi-dots-horizontal";
+        default:
+          return "$warning";
+      }
+    },
+    isAbleToClick() {
+      return (
+        this.status === this.$options.CHANGES ||
+        this.status === this.$options.ERROR
+      );
+    },
+  },
+  methods: {
+    handleClick() {
+      if (this.isAbleToClick) {
+        emit('manual-update');
+      }
+    }
+  }
+};
+</script>
diff --git a/aleksis/core/frontend/messages/en.json b/aleksis/core/frontend/messages/en.json
index 3adabc47bc60d1bfca2dfd74b0c4b31591c2ea88..ff9693d369fea8f9ce44e07d5cc1dd4b89aae1aa 100644
--- a/aleksis/core/frontend/messages/en.json
+++ b/aleksis/core/frontend/messages/en.json
@@ -224,5 +224,11 @@
   },
   "graphql": {
     "snackbar_error_message": "There was an error retrieving the page data. Please try again."
+  },
+  "status": {
+    "saved": "All changes are saved.",
+    "updating": "Changes are being synced.",
+    "changes": "You have unsaved changes.",
+    "error": "There has been an error while saving the latest changes."
   }
 }