EpLazyImage
Props
| Name | Description | Type | Default |
|---|---|---|---|
src | The source URL of the image. | string | - |
alt | The alt text for the image. | string | '' |
width | The width of the image. | `string | number` |
height | The height of the image. | `string | number` |
aspectRatio | The aspect ratio of the image (e.g., '16 / 9'). | string | '16 / 10' |
objectFit | How the image fits within its container. | string | 'contain' |
className | Additional CSS class name for the image element. | string | '' |
placeholder | URL of the placeholder image to display while loading. | string | '' |
placeholderColor | The background color of the placeholder. | string | '#f5f5f5' |
placeholderOpacity | The opacity of the placeholder. | number | 1 |
lazy | If true, enables lazy loading using Intersection Observer. | boolean | true |
rounded | If true, applies rounded corners to the image. | boolean | true |
rootMargin | The root margin for the Intersection Observer (controls when loading starts). | string | '0px 0px 100px 0px' |
INFO
This component does not use events, slots.
Component Code
vue
<template>
<div
ref="imageEl"
:class="['ep-image', { 'ep-image--rounded': rounded, 'ep-image--loading': !isLoaded }]"
>
<img
v-if="isLoaded"
:src="src"
:alt="alt"
:width="width"
:height="height"
:class="['ep-image__img', className]"
:style="{ aspectRatio: aspectRatio, objectFit: objectFit }"
>
<div
v-else
class="ep-image__placeholder"
:style="placeholderStyle"
/>
</div>
</template>
<script setup>
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
const props = defineProps({
/**
* The source URL of the image.
*/
src: {
type: String,
required: true,
},
/**
* The alt text for the image.
*/
alt: {
type: String,
default: '',
},
/**
* The width of the image.
*/
width: {
type: [String, Number],
default: '100%',
},
/**
* The height of the image.
*/
height: {
type: [String, Number],
default: '100%',
},
/**
* The aspect ratio of the image (e.g., '16 / 9').
*/
aspectRatio: {
type: String,
default: '16 / 10',
},
/**
* How the image fits within its container.
* @values contain, cover, fill, none, scale-down
*/
objectFit: {
type: String,
default: 'contain',
},
/**
* Additional CSS class name for the image element.
*/
className: {
type: String,
default: '',
},
/**
* URL of the placeholder image to display while loading.
*/
placeholder: {
type: String,
default: '',
},
/**
* The background color of the placeholder.
*/
placeholderColor: {
type: String,
default: '#f5f5f5',
},
/**
* The opacity of the placeholder.
*/
placeholderOpacity: {
type: Number,
default: 1,
},
/**
* If true, enables lazy loading using Intersection Observer.
*/
lazy: {
type: Boolean,
default: true,
},
/**
* If true, applies rounded corners to the image.
*/
rounded: {
type: Boolean,
default: true,
},
/**
* The root margin for the Intersection Observer (controls when loading starts).
*/
rootMargin: {
type: String,
default: '0px 0px 100px 0px',
},
})
const isLoaded = ref(false)
const imageEl = ref(null)
let observer = null
const placeholderStyle = computed(() => {
return {
width: props.width,
height: props.height,
aspectRatio: props.aspectRatio,
backgroundColor: props.placeholderColor,
opacity: props.placeholderOpacity,
backgroundImage: props.placeholder ? `url(${props.placeholder})` : '',
backgroundSize: 'cover',
}
})
const addLazyLoadListener = () => {
observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
loadImage()
observer.unobserve(entry.target)
}
})
}, {
rootMargin: props.rootMargin,
threshold: 0.1,
})
if (imageEl.value) {
observer.observe(imageEl.value)
}
}
const loadImage = () => {
isLoaded.value = true
}
onMounted(() => {
if (props.lazy) {
addLazyLoadListener()
} else {
loadImage()
}
})
onBeforeUnmount(() => {
if (observer) {
observer.disconnect()
observer = null
}
})
</script>
<style lang="scss" scoped>
.ep-image {
position: relative;
display: block;
overflow: hidden;
max-width: 100%;
&--rounded {
border-radius: var(--border-radius--large);
}
&--loading {
background: var(--interface-surface);
&::after {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
animation: shimmer 1.5s infinite;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
content: '';
}
}
}
.ep-image__img {
display: block;
width: 100%;
height: auto;
opacity: 0;
transition: opacity 0.4s ease-in-out;
animation: fadeIn 0.4s ease-in-out forwards;
}
.ep-image__placeholder {
position: absolute;
top: 0;
left: 0;
}
@keyframes fadeIn {
to {
opacity: 1;
}
}
@keyframes shimmer {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(100%);
}
}
</style>