diff --git a/apps/files/src/components/FileEntry/FileEntryPreview.vue b/apps/files/src/components/FileEntry/FileEntryPreview.vue
index 2e43338cf0558..817dac40b40e2 100644
--- a/apps/files/src/components/FileEntry/FileEntryPreview.vue
+++ b/apps/files/src/components/FileEntry/FileEntryPreview.vue
@@ -14,16 +14,19 @@
 			</template>
 		</template>
 
-		<!-- Decorative image, should not be aria documented -->
-		<img v-else-if="previewUrl && backgroundFailed !== true"
-			ref="previewImg"
-			alt=""
-			class="files-list__row-icon-preview"
-			:class="{'files-list__row-icon-preview--loaded': backgroundFailed === false}"
-			loading="lazy"
-			:src="previewUrl"
-			@error="onBackgroundError"
-			@load="backgroundFailed = false">
+		<!-- Decorative images, should not be aria documented -->
+		<span v-else-if="previewUrl" class="files-list__row-icon-preview-container">
+			<canvas v-if="hasBlurhash && (backgroundFailed === true || !backgroundLoaded)" ref="canvas" class="files-list__row-icon-blurhash" />
+			<img v-if="backgroundFailed !== true"
+				ref="previewImg"
+				alt=""
+				class="files-list__row-icon-preview"
+				:class="{'files-list__row-icon-preview--loaded': backgroundFailed === false}"
+				loading="lazy"
+				:src="previewUrl"
+				@error="onBackgroundError"
+				@load="onBackgroundLoad">
+		</span>
 
 		<FileIcon v-else v-once />
 
@@ -58,6 +61,7 @@ import LinkIcon from 'vue-material-design-icons/Link.vue'
 import NetworkIcon from 'vue-material-design-icons/Network.vue'
 import TagIcon from 'vue-material-design-icons/Tag.vue'
 import PlayCircleIcon from 'vue-material-design-icons/PlayCircle.vue'
+import { decode } from 'blurhash'
 
 import CollectivesIcon from './CollectivesIcon.vue'
 import FavoriteIcon from './FavoriteIcon.vue'
@@ -107,6 +111,7 @@ export default Vue.extend({
 	data() {
 		return {
 			backgroundFailed: undefined as boolean | undefined,
+			backgroundLoaded: false as boolean,
 		}
 	},
 
@@ -206,6 +211,16 @@ export default Vue.extend({
 
 			return null
 		},
+
+		hasBlurhash() {
+			return this.source.attributes['metadata-blurhash'] !== undefined
+		},
+	},
+
+	mounted() {
+		if (this.hasBlurhash && this.$refs.canvas) {
+			this.drawBlurhash()
+		}
 	},
 
 	methods: {
@@ -213,17 +228,36 @@ export default Vue.extend({
 		reset() {
 			// Reset background state to cancel any ongoing requests
 			this.backgroundFailed = undefined
+			this.backgroundLoaded = false
 			if (this.$refs.previewImg) {
 				this.$refs.previewImg.src = ''
 			}
 		},
 
+		onBackgroundLoad(event) {
+			this.backgroundFailed = false
+			this.backgroundLoaded = true
+		},
+
 		onBackgroundError(event) {
 			// Do not fail if we just reset the background
 			if (event.target?.src === '') {
 				return
 			}
 			this.backgroundFailed = true
+			this.backgroundLoaded = false
+		},
+
+		drawBlurhash() {
+			const width = this.$refs.canvas.width
+			const height = this.$refs.canvas.height
+
+			const pixels = decode(this.source.attributes['metadata-blurhash'], width, height)
+
+			const ctx = this.$refs.canvas.getContext('2d')
+			const imageData = ctx.createImageData(width, height)
+			imageData.data.set(pixels)
+			ctx.putImageData(imageData, 0, 0)
 		},
 
 		t,
diff --git a/apps/files/src/components/FilesListVirtual.vue b/apps/files/src/components/FilesListVirtual.vue
index 75f5792498435..068a3cb9e64e8 100644
--- a/apps/files/src/components/FilesListVirtual.vue
+++ b/apps/files/src/components/FilesListVirtual.vue
@@ -557,11 +557,24 @@ export default defineComponent({
 				}
 			}
 
-			&-preview {
+			&-preview-container {
+				position: relative; // Needed for the blurshash to be positioned correctly
 				overflow: hidden;
 				width: var(--icon-preview-size);
 				height: var(--icon-preview-size);
 				border-radius: var(--border-radius);
+			}
+
+			&-blurhash {
+				position: absolute;
+				top: 0;
+				left: 0;
+				height: 100%;
+				width: 100%;
+				object-fit: cover;
+			}
+
+			&-preview {
 				// Center and contain the preview
 				object-fit: contain;
 				object-position: center;
diff --git a/apps/files/src/init.ts b/apps/files/src/init.ts
index db4aec7fa0633..0f4ef70b1b2b6 100644
--- a/apps/files/src/init.ts
+++ b/apps/files/src/init.ts
@@ -66,5 +66,6 @@ registerPreviewServiceWorker()
 
 registerDavProperty('nc:hidden', { nc: 'http://nextcloud.org/ns' })
 registerDavProperty('nc:is-mount-root', { nc: 'http://nextcloud.org/ns' })
+registerDavProperty('nc:metadata-blurhash', { nc: 'http://nextcloud.org/ns' })
 
 initLivePhotos()
diff --git a/package-lock.json b/package-lock.json
index a54e042dd5363..046bba8f8b2cb 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -38,6 +38,7 @@
         "@vueuse/integrations": "^11.0.1",
         "backbone": "^1.4.1",
         "blueimp-md5": "^2.19.0",
+        "blurhash": "^2.0.5",
         "browserslist-useragent-regexp": "^4.1.1",
         "camelcase": "^8.0.0",
         "cancelable-promise": "^4.3.1",
@@ -8111,6 +8112,11 @@
       "integrity": "sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w==",
       "license": "MIT"
     },
+    "node_modules/blurhash": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/blurhash/-/blurhash-2.0.5.tgz",
+      "integrity": "sha512-cRygWd7kGBQO3VEhPiTgq4Wc43ctsM+o46urrmPOiuAe+07fzlSB9OJVdpgDL0jPqXUVQ9ht7aq7kxOeJHRK+w=="
+    },
     "node_modules/bmp-js": {
       "version": "0.1.0",
       "resolved": "https://registry.npmjs.org/bmp-js/-/bmp-js-0.1.0.tgz",
diff --git a/package.json b/package.json
index 2a0efe599ce98..2d9ffcc856d9b 100644
--- a/package.json
+++ b/package.json
@@ -66,6 +66,7 @@
     "@vueuse/integrations": "^11.0.1",
     "backbone": "^1.4.1",
     "blueimp-md5": "^2.19.0",
+    "blurhash": "^2.0.5",
     "browserslist-useragent-regexp": "^4.1.1",
     "camelcase": "^8.0.0",
     "cancelable-promise": "^4.3.1",