diff --git a/projects/frontend/src/views/project/project-list.vue b/projects/frontend/src/views/project/project-list.vue
index 2281331..504715d 100644
--- a/projects/frontend/src/views/project/project-list.vue
+++ b/projects/frontend/src/views/project/project-list.vue
@@ -10,21 +10,51 @@ import { fetchAndParseYaml } from 'src/utilities/fetch'
 import { getCurrentRoute } from 'src/utilities/vuetils'
 import { useRouteStore } from 'src/routes'
 
+import FilterPanel from 'src/components/shared/filter-panel.vue'
 import ProjectTile from './project-tile.vue'
 
+/**
+ * A wrapper around {@link GalleryEntries} for the app's use only which adds additional fields
+ * in order for the app to effectively display the entries.
+ */
+ type ProjectDisplayedEntries = { [idOrTitle: string]: ProjectListingInfo & {
+  /**
+   * specifies whether the entry is hidden by the tags selected by a visitor
+   */
+  hidden?: boolean
+}}
+
 const projectIds = ref([] as string[])
-const projects = ref({} as { [key: string]: ProjectListingInfo })
+const projects = ref({} as ProjectDisplayedEntries)
 const ready = ref(false)
 const currentRoute = getCurrentRoute()
 const routeStore = useRouteStore()
 const routeConfig = routeStore._routes[currentRoute.path] as ProjectListDefinition
+const config = ref(null as ProjectList | null)
+
+/**
+ * Handler for a tag being selected;
+ * updates the visibility state of the current entries
+ * @param tagsToggled: the tags currently toggled in the filter panel
+ */
+ const onToggledTagsChanged = (tagsToggled: string[]) => {
+  if (tagsToggled.length < 1) {
+    Object.keys(projects.value).forEach(entryId => {
+      projects.value[entryId].hidden = false
+    })
+  } else {
+    Object.keys(projects.value).forEach(entryId => {
+      projects.value[entryId].hidden = !projects.value[entryId].tags?.some(own => tagsToggled.includes(own))
+    })
+  }
+}
 
 onMounted(async () => {
-  const config = await fetchAndParseYaml<ProjectList>(routeConfig.config)
-  projectIds.value = Object.keys(config.projects)
+  config.value = await fetchAndParseYaml<ProjectList>(routeConfig.config)
+  projectIds.value = Object.keys(config.value.projects)
   for (let i = 0; i < projectIds.value.length; ++i) {
     const id = projectIds.value[i]
-    projects.value[id] = await fetchAndParseYaml(config.projects[id].config)
+    projects.value[id] = await fetchAndParseYaml(config.value.projects[id].config)
   }
   document.title = routeConfig.title
   ready.value = true
@@ -36,11 +66,19 @@ onMounted(async () => {
   #projects(
     v-if='ready'
   )
-    ProjectTile(
+    Transition(
       v-for='id in projectIds'
-      :id='id'
-      :info='projects[id]'
     )
+      ProjectTile(
+        v-if='!projects[id].hidden'
+        :id='id'
+        :info='projects[id]'
+      )
+  FilterPanel(
+    v-if='ready && !!config.tags'
+    :tags='config.tags'
+    @toggledTagsChanged='onToggledTagsChanged($event)'
+  )
 </template>
 
 <style scoped lang="sass">