You've seen it at least a couple of times already: Those shiny box effects when images take a little longer to load. They're on news sites, blogs, shops, you name it. In this article, I'll explain how to build a component in Vue that offers this effect!
Some scaffolding
I'll start with a new component I call LoadingBoxImage.vue
- this will be a single file component, so it can be used within any Vue or Nuxt app afterwards. First, I'll basically wrap the <img>
tag:
<!-- LoadingBoxImage.vue -->
<template>
<img :src="src" :alt="alt" />
</template>
<script>
export default {
props: {
src: {
type: String,
required: true
},
alt: {
type: String,
required: true
}
}
}
</script>
Now I'll add some styling to this, so it behaves well and more responsive:
<style scoped>
img {
max-width: 100%;
}
</style>
This component can be used like this:
<!-- App.vue -->
<template>
<div id="app">
<loading-box-image
src="http://via.placeholder.com/3200x2400"
alt="Some image"
/>
</div>
</template>
<script>
import LoadingBoxImage from './components/LoadingBoxImage'
export default {
components: {
LoadingBoxImage
}
}
</script>
So far, so good. I've replicated the standard image tag as a Vue component.
Next up: Adding a box and removing it again.
The placeholder
The placeholder box will be an aria-hidden div next to the image. I'll hide it once the image has loaded via the native load
event and a flag:
<template>
<div class="image-container">
<img :src="src" :alt="alt" @load="loaded" />
<div
class="image-placeholder"
:class="{ hidden: isLoaded }"
aria-hidden="true"
/>
</div>
</template>
<script>
export default {
props: {
src: {
type: String,
required: true
},
alt: {
type: String,
required: true
}
},
data() {
return {
isLoaded: false
}
},
methods: {
loaded() {
this.isLoaded = true
}
}
}
</script>
<style scoped>
.image-container, img {
max-width: 100%;
}
.image-placeholder.hidden {
display: none;
}
</style>
I also needed to add a container around the image and its placeholder and did some adjustments to the styling.
Now, ideally, the placeholder should be the same size as the image, right? I've got two options here: Use fixed dimensions or try to fetch them before the image has fully loaded. Since the latter sounds a lot fancier, I'll implement this first.
At some point, the image will have nativeWidth
and nativeHeight
available, so I can use those to calculate an aspect ratio:
// LoadingBoxImage.vue, script part
mounted() {
const img = this.$refs.img
// Constantly poll for the naturalWidth
const sizeInterval = setInterval(() => {
if (img.naturalWidth) {
// As soon as available: Stop polling
clearInterval(sizeInterval)
// Calculate image ratio
this.loadedAspectRatio = img.naturalWidth / img.naturalHeight
}
}, 10)
}
(I also added ref
attributes to the original <img>
tag and the placeholder to be able to fetch the necessary data)
I can use that now to calculate the placeholder's height. To make it more responsive, I'm updating the client width on the window's resize
event and set it once when mounted:
// ...
data() {
return {
isLoaded: false,
loadedAspectRatio: null,
clientWidth: 0,
};
},
methods: {
loaded() {
this.isLoaded = true;
},
updateClientWidth() {
this.clientWidth = this.$refs.placeholder.clientWidth;
}
},
computed: {
/**
* Calculates the height of the placeholder
* via the images nativeWidth and nativeHeight.
*/
placeholderHeight() {
if (!this.loadedAspectRatio) {
return 0;
}
return this.clientWidth / this.loadedAspectRatio;
},
},
mounted() {
const img = this.$refs.img;
const sizeInterval = setInterval(() => {
if (img.naturalWidth) {
clearInterval(sizeInterval);
this.loadedAspectRatio = img.naturalWidth / img.naturalHeight;
}
}, 10);
window.addEventListener('resize', this.updateClientWidth)
this.updateClientWidth()
},
// ...
And set this on the placeholder:
<!-- ... -->
<div
class="image-placeholder"
:class="{ hidden: isLoaded }"
aria-hidden="true"
ref="placeholder"
:style="{ height: `${placeholderHeight}px` }"
/>
<!-- ... -->
Ok, now I've got a placeholder the same size as the image! Awesome! Now I can start to...
Add the shiny box effect
This can be done with CSS keyframes and linear gradient:
.image-placeholder {
background: rgba(0,0,0,.2);
background-image: linear-gradient(
120deg,
rgba(255,255,255,0) 0%,
rgba(255,255,255,0) 40%,
rgba(255,255,255,0.8) 50%,
rgba(255,255,255,0) 60%,
rgba(255,255,255,0) 100%
);
background-position-x: -100vw;
background-repeat: no-repeat;
animation: shiny 1.5s infinite;
}
@keyframes shiny {
0% {
background-position-x: -100vw;
}
10% {
background-position-x: -100vw;
}
75% {
background-position-x: 100vw;
}
100% {
background-position-x: 100vw;
}
}
This will add a single reflection that moves periodically from left to right to an otherwise grayed element.
And that's it!
Here's a Codesandbox to see it in action (I'm not hiding the placeholder for you to see what it would look like):
I'm sure that the gradient and the timing can still be tweaked, though. Also some use cases, like smaller images and complete accessibility are missing, but I'm certain that this can be used as a starting point.
Happy holidays!
I hope you enjoyed reading this article as much as I enjoyed writing it! If so, leave a β€οΈ or a π¦! I write tech articles in my free time and like to drink coffee every once in a while.
If you want to support my efforts, please consider buying me a coffee β or follow me on Twitter π¦!
Top comments (0)