diff --git a/js/store/elements.js b/js/store/elements.js
index 6fe2ed7c855ce764f752130b62e3e40d64ccde83..9647b7ab52260b7dd7bb311e35cdf015a5ba2c5f 100644
--- a/js/store/elements.js
+++ b/js/store/elements.js
@@ -630,9 +630,19 @@ export const actions = {
   }
 }
 
+export const getters = {
+  canWrite: state => id => {
+    return (state.elements[id]?.rights ?? []).includes('write')
+  },
+  canAdmin: state => id => {
+    return (state.elements[id]?.rights ?? []).includes('admin')
+  }
+}
+
 export default {
   namespaced: true,
   state: initialState(),
   mutations,
-  actions
+  actions,
+  getters
 }
diff --git a/test/store/elements.js b/test/store/elements.js
index b03e83dad8087c416ee78cdeb2d13bfdc4d67ffc..f30594b5beebc9c2f8d1e0a8ac0e8aba4ff7dc54 100644
--- a/test/store/elements.js
+++ b/test/store/elements.js
@@ -1,7 +1,7 @@
 import assert from 'assert'
 import axios from 'axios'
 import { pick } from 'lodash'
-import { mutations } from '~/js/store/elements'
+import { mutations, getters } from '~/js/store/elements'
 import { assertRejects, assertThrows, FakeAxios } from '~/test/testhelpers'
 import store from '~/test/store'
 import { elementNeighborsSample, jobsSample, transcriptionsPage1, transcriptionsPage2 } from '~/test/samples'
@@ -1789,4 +1789,86 @@ describe('elements', () => {
       })
     })
   })
+
+  describe('getters', () => {
+    describe('canWrite', () => {
+      it('returns true if the user has write access on an element', () => {
+        const getter = getters.canWrite({
+          elements: {
+            elementid: {
+              rights: ['read', 'write']
+            }
+          }
+        })
+        assert.ok(getter('elementid'))
+      })
+
+      it('returns false if the user does not have write access', () => {
+        const getter = getters.canWrite({
+          elements: {
+            elementid: {
+              rights: ['read', 'admin']
+            }
+          }
+        })
+        assert.ok(!getter('elementid'))
+      })
+
+      it('returns false if there are no access rights on the element', () => {
+        const getter = getters.canWrite({
+          elements: {
+            elementid: {
+              id: 'elementid'
+            }
+          }
+        })
+        assert.ok(!getter('elementid'))
+      })
+
+      it('returns false for nonexistent elements', () => {
+        const getter = getters.canWrite({ elements: {} })
+        assert.ok(!getter('elementid'))
+      })
+    })
+
+    describe('canAdmin', () => {
+      it('returns true if the user has admin access on an element', () => {
+        const getter = getters.canAdmin({
+          elements: {
+            elementid: {
+              rights: ['read', 'admin']
+            }
+          }
+        })
+        assert.ok(getter('elementid'))
+      })
+
+      it('returns false if the user does not have admin access', () => {
+        const getter = getters.canAdmin({
+          elements: {
+            elementid: {
+              rights: ['read', 'write']
+            }
+          }
+        })
+        assert.ok(!getter('elementid'))
+      })
+
+      it('returns false if there are no access rights on the element', () => {
+        const getter = getters.canAdmin({
+          elements: {
+            elementid: {
+              id: 'elementid'
+            }
+          }
+        })
+        assert.ok(!getter('elementid'))
+      })
+
+      it('returns false for nonexistent elements', () => {
+        const getter = getters.canAdmin({ elements: {} })
+        assert.ok(!getter('elementid'))
+      })
+    })
+  })
 })
diff --git a/vue/Element/DetailsPanel.vue b/vue/Element/DetailsPanel.vue
index f7ff357a4e1cedab93f34bec446f01e9cdcd096c..02a02a18f37870dcb0568374acd5493ccb6d91b5 100644
--- a/vue/Element/DetailsPanel.vue
+++ b/vue/Element/DetailsPanel.vue
@@ -21,9 +21,9 @@
       </router-link>
       <a
         class="icon-trash"
-        :class="canAdmin(corpus) ? 'has-text-danger' : 'has-text-grey-light'"
-        :title="canAdmin(corpus) ? 'Delete this element' : 'Admin access on the project is required to delete this element'"
-        v-on:click="deleteModal = canAdmin(corpus)"
+        :class="canAdminElement(elementId) ? 'has-text-danger' : 'has-text-grey-light'"
+        :title="canAdminElement(elementId) ? 'Delete this element' : 'Admin access on the project is required to delete this element'"
+        v-on:click="deleteModal = canAdminElement(elementId)"
       ></a>
 
       <Modal v-model="deleteModal" :title="'Delete ' + typeName(element.type) + ' ' + element.name">
@@ -48,9 +48,9 @@
         <hr />
       </template>
 
-      <DropdownContent title="Classifications" :disabled="(!canWrite(corpus) || !hasMlClasses) && !classifications.length">
+      <DropdownContent title="Classifications" :disabled="(!canWriteElement(elementId) || !hasMlClasses) && !classifications.length">
         <HelpModal class="is-pulled-right" title="Help for classifications" :data="CLASSIFICATIONS_HELP" />
-        <div v-if="canWrite(corpus) && hasMlClasses" class="field has-addons">
+        <div v-if="canWriteElement(elementId) && hasMlClasses" class="field has-addons">
           <p class="control">
             <MLClassSelect
               ref="newClassificationSelect"
@@ -85,7 +85,7 @@
             :transcriptions="transcriptionGroup[1]"
             :worker-id="transcriptionGroup[0]"
           />
-          <template v-if="canWrite(corpus)">
+          <template v-if="canWriteElement(elementId)">
             <a v-on:click="transcriptionModal = true">
               <i class="icon-add-square"></i>
               Add
@@ -104,7 +104,7 @@
         <hr />
       </template>
 
-      <DropdownContent title="Metadata" v-model="toggleMetas" :disabled="!metadata.length && !canWrite(corpus)">
+      <DropdownContent title="Metadata" v-model="toggleMetas" :disabled="!metadata.length && !canWriteElement(elementId)">
         <ElementMetadata :corpus-id="element.corpus.id" :element-id="element.id" :opened="toggleMetas" />
       </DropdownContent>
       <hr />
@@ -120,7 +120,7 @@
 </template>
 
 <script>
-import { mapState, mapActions } from 'vuex'
+import { mapState, mapActions, mapGetters } from 'vuex'
 import { corporaMixin } from '~/js/mixins'
 import { CLASSIFICATIONS_HELP } from '~/js/help'
 import { MANUAL_WORKER_VERSION } from '~/js/config'
@@ -185,6 +185,11 @@ export default {
   computed: {
     ...mapState('elements', ['elements', 'transcriptions', 'neighbors']),
     ...mapState('process', ['workerVersions', 'workers']),
+    ...mapGetters('elements', {
+      // canWrite and canAdmin are already defined in corporaMixin
+      canWriteElement: 'canWrite',
+      canAdminElement: 'canAdmin'
+    }),
     element () {
       return this.elements[this.elementId]
     },
@@ -232,7 +237,7 @@ export default {
       }
     },
     async performDelete () {
-      if (!this.canAdmin(this.corpus) || this.deleteLoading) return
+      if (!this.canAdminElement(this.elementId) || this.deleteLoading) return
       this.deleteLoading = true
       try {
         await this.$store.dispatch('elements/delete', { id: this.elementId })
@@ -277,10 +282,15 @@ export default {
   watch: {
     elementId: {
       immediate: true,
-      async handler (id) {
+      handler (id) {
         if (!id) return
-        // Prevent retrieving element details multiple times
-        if (this.element?.id !== id) await this.$store.dispatch('elements/get', { id })
+        /*
+         * Do not retrieve the element again if it already exists in the store,
+         * unless it lacks the `rights` attribute: this attribute is only available from RetrieveElement,
+         * and some elements in the store can come from list endpoints such as those of the children tree.
+         * This ensures there are no strange behaviors where some actions are only sometimes disabled when they shouldn't.
+         */
+        if (this.element?.id !== id || !this.element?.rights) this.$store.dispatch('elements/get', { id })
       }
     },
     elementType: {
diff --git a/vue/Element/EditionForm.vue b/vue/Element/EditionForm.vue
index b33807e9d43849cd6ef1917b95752ec844aae8de..48cd2d07b896ccaf779ae1b766d157b74563faac 100644
--- a/vue/Element/EditionForm.vue
+++ b/vue/Element/EditionForm.vue
@@ -30,7 +30,7 @@
               <div class="field">
                 <div class="control">
                   <input
-                    :disabled="!canWrite(corpus)"
+                    :disabled="!canWriteElement(element.id)"
                     type="text"
                     class="input mousetrap"
                     :class="{ 'is-danger': fieldErrors.name }"
@@ -54,7 +54,7 @@
                 <div class="control">
                   <div class="control" title="Filter by type">
                     <span class="select is-fullwidth" :class="{ 'is-danger': fieldErrors.type }">
-                      <select v-model="type" class="mousetrap" :disabled="!canWrite(corpus)">
+                      <select v-model="type" class="mousetrap" :disabled="!canWriteElement(element.id)">
                         <option value="" disabled selected>Type…</option>
                         <option v-for="t in corpus.types" :key="t.slug" :value="t.slug">
                           {{ t.display_name | truncateSelect }}
@@ -83,9 +83,9 @@
       </router-link>
       <span
         class="button is-danger"
-        :disabled="loading || !canAdmin(corpus)"
+        :disabled="!canDelete"
         :class="{ 'is-loading': deleteLoading }"
-        :title="canAdmin(corpus) ? 'Delete this element and its children' : 'A project administrator right is required to delete this element and its children'"
+        :title="canDelete ? 'Delete this element and its children' : 'A project administrator right is required to delete this element and its children'"
         v-on:click="deleteElement"
       >
         <i class="icon-trash"></i>
@@ -95,7 +95,7 @@
       <button
         class="button is-success has-margin-left"
         :class="{ 'is-loading': updateLoading }"
-        :disabled="loading || !canUpdate || !canWrite(corpus)"
+        :disabled="loading || !canUpdate"
         :title="canUpdateTitle"
         v-on:click="updateElement"
       >
@@ -106,7 +106,7 @@
 </template>
 
 <script>
-import { mapMutations, mapActions, mapState } from 'vuex'
+import { mapState, mapMutations, mapGetters } from 'vuex'
 import { ELEMENT_NAME_MAX_LENGTH } from '~/js/config'
 import { errorParser } from '~/js/helpers'
 import { truncateMixin, corporaMixin } from '~/js/mixins'
@@ -147,6 +147,11 @@ export default {
   computed: {
     ...mapState('annotation', { defaultType: 'type' }),
     ...mapState('elements', ['selectedElement']),
+    ...mapGetters('elements', {
+      // canWrite and canAdmin are already defined on corporaMixin
+      canWriteElement: 'canWrite',
+      canAdminElement: 'canAdmin'
+    }),
     loading () {
       return this.deleteLoading || this.updateLoading
     },
@@ -163,20 +168,24 @@ export default {
       return this.type && this.validClassification
     },
     canUpdate () {
-      // Type and name are valid and different from elements current values
-      if (!this.type) return false
-      return this.type !== this.element.type || (this.name && this.name !== this.element.name)
+      // User has write access and type and name are valid and different from the element's current values
+      return !this.loading && this.canWriteElement(this.element.id) && (
+        (this.type && this.type !== this.element.type) ||
+        (this.name && this.name !== this.element.name)
+      )
     },
     canUpdateTitle () {
-      if (!this.canWrite(this.corpus)) return 'A project write access is required to update this element'
+      if (!this.canWriteElement(this.element.id)) return 'A project write access is required to update this element'
       if (!this.canUpdate) return 'A valid updated type and name are required to update the element'
       return 'Update element'
+    },
+    canDelete () {
+      return !this.loading && this.canAdminElement(this.element.id)
     }
   },
   methods: {
     ...mapMutations('notifications', ['notify']),
     ...mapMutations('elements', ['selectElement']),
-    ...mapActions('elements', ['patch', 'delete']),
     nameFromElement () {
       // Update name input to default value if not defined
       if (!this.name) this.name = this.element.name
@@ -188,7 +197,7 @@ export default {
       else this.fieldErrors = error.response.data
     },
     async updateElement () {
-      if (this.loading || !this.canUpdate || !this.canWrite(this.corpus)) return
+      if (this.loading || !this.canUpdate || !this.canWriteElement(this.element.id)) return
       this.updateLoading = true
       this.setErrors(null)
       this.nameFromElement()
@@ -209,7 +218,7 @@ export default {
       }
     },
     async deleteElement () {
-      if (!this.corpus || !this.canAdmin(this.corpus)) return
+      if (!this.canDelete) return
       this.deleteLoading = true
       try {
         await this.$store.dispatch('elements/delete', { id: this.element.id })
diff --git a/vue/HeaderActions.vue b/vue/HeaderActions.vue
index 79f33085d70966e9ff8f520a3087a0b204a4bf87..6aeb8f222166a5f2f381a05e8f50393703f1065d 100644
--- a/vue/HeaderActions.vue
+++ b/vue/HeaderActions.vue
@@ -496,6 +496,11 @@ export default {
       'isVerified',
       'hasFeature'
     ]),
+    ...mapGetters('elements', {
+      // canWrite and canAdmin are already defined in corporaMixin
+      canWriteElement: 'canWrite',
+      canAdminElement: 'canAdmin'
+    }),
     ...mapGetters('navigation', { canDeleteFiltered: 'canDelete' }),
     ...mapState('navigation', { filteredElements: 'elements' }),
     ...mapState('elements', [
@@ -538,10 +543,24 @@ export default {
       return firstParents.filter((parent, index, parents) => parents.findIndex(p => (p.id === parent.id)) === index)
     },
     hasContribPrivilege () {
-      return this.isVerified && this.corpus && this.canWrite(this.corpus)
+      /*
+       * Either these actions are shown for an element and the user has write rights on the element,
+       * or the user has write rights on the corpus.
+       */
+      return this.isVerified && (
+        (this.elementId && this.canWriteElement(this.elementId)) ||
+        (this.corpus && this.canWrite(this.corpus))
+      )
     },
     hasAdminPrivilege () {
-      return this.isVerified && this.corpus && this.canAdmin(this.corpus)
+      /*
+       * Either these actions are shown for an element and the user has admin rights on the element,
+       * or the user has admin rights on the corpus.
+       */
+      return this.isVerified && (
+        (this.elementId && this.canAdminElement(this.elementId)) ||
+        (this.corpus && this.canAdmin(this.corpus))
+      )
     },
     canContain () {
       // Determine if the current view show a corpus or a folder