<div class="media">
<p-lazy>
<video class="media__content" inert controls autoplay playsinline>
<source data-src="https://www.w3schools.com/html/mov_bbb.mp4" type="video/mp4">
Your browser does not support the video tag.
</video>
<template #placeholder="{ load, modifier }">
<picture class="media__mask" :class="modifier">
<img srcset="https://unsplash.it/320/176?random&gravity=center" alt="">
</picture>
<button class="media__trigger" @click="load" :class="modifier">
<span class="srOnly">play</span>
<svg class="media__icon icon -circle" viewBox="0 0 96 96"><use href="/main-icons-sprite.svg#play" /></svg>
</button>
</template>
</p-lazy>
</div>
<div class="media">
<p-lazy>
<video inert controls autoplay playsinline>
<source data-src="https://www.w3schools.com/html/mov_bbb.mp4" type="video/mp4">
Your browser does not support the video tag.
</video>
<template #placeholder="{ load, modifier }">
<picture class="media__mask" :class="modifier">
<source srcset="https://unsplash.it/320/176?random&gravity=center" media="(min-width: 768px)">
<source srcset="https://unsplash.it/320/176?random&gravity=center" media="(min-width: 480px)">
<img class="lazyload" srcset="https://unsplash.it/320/176?random&gravity=center" alt="">
</picture>
<button class="media__trigger" :class="modifier" @click="load">
<span class="srOnly">play</span>
<svg class="media__icon icon -circle -light" viewBox="0 0 96 96"><use href="/main-icons-sprite.svg#play" /></svg>
</button>
</template>
</div>
<br>
<div class="media">
<p-lazy>
<iframe data-src="https://player.vimeo.com/video/818944000?h=e8674039c8&autoplay=1&title=0&byline=0" frameborder="0" allow="autoplay; fullscreen; picture-in-picture" allowfullscreen></iframe>
<template #placeholder="{ load, modifier }">
<picture class="media__mask" :class="modifier">
<source srcset="https://unsplash.it/267/150?random&gravity=center" media="(min-width: 768px)">
<source srcset="https://unsplash.it/267/150?random&gravity=center" media="(min-width: 480px)">
<img class="lazyload" srcset="https://unsplash.it/267/150?random&gravity=center" alt="">
</picture>
<button class="media__trigger" :class="modifier" @click="load">
<span class="srOnly">play</span>
<svg class="media__icon icon -circle" viewBox="0 0 96 96"><use href="/main-icons-sprite.svg#play" /></svg>
</button>
</template>
</p-lazy>
</div>
.media {
$b: &;
aspect-ratio: var(--aspect-ratio);
background-color: rgb(from currentColor r g b / .25);
display: inline grid;
margin-bottom: 2rem;
> * {
grid-area: 1 / 1;
position: relative;
}
&__mask {
margin: 0;
display: grid;
transition: opacity var(--root-ease-out-moderate);
&.-loaded {
opacity: 0;
pointer-events: none;
}
}
&__maskImg {
width: 100%;
height: 100%;
object-fit: cover;
}
&__trigger {
appearance: none;
background: #0000;
border: 0;
border-radius: max(50cqmin, 6rem);
padding: 0;
align-self: center;
justify-self: center;
color: var(--accent-color-200);
container-type: size;
position: relative;
transition: color var(-root-ease-out-fast),
opacity var(--root-ease-out-moderate);
width: min(50cqmin, 6rem);
height: min(50cqmin, 6rem);
&:hover {
color: var(--accent-color-300);
}
&.-loaded {
align-self: start;
height: min(25cqmin, 3rem);
justify-self: end;
margin: .5rem;
opacity: 0;
pointer-events: none;
transition: opacity var(--root-ease-out-fast);
width: min(25cqmin, 3rem);
&.-keepVisible {
opacity: .25;
pointer-events: auto;
}
&.-keepVisible:hover,
&:focus-visible {
opacity: 1;
}
}
}
&__icon {
--icon-stroke-color: var(--accent-color-200);
filter: drop-shadow(var(--root-box-shadow-med));
width: auto;
height: auto;
&.-circle {
padding: min(6cqmin, .75rem);
transition: padding var(--root-ease-out-fast);
&:hover {
padding: min(4cqmin, .5rem);
}
}
}
}
<script setup>
import { computed, ref, nextTick, useTemplateRef } from 'vue'
/**
* PLazy is a vue component that lazy loads its content after it's been clicked.
*
* @slot default - the content to lazyload. It will remove inert attributes, change data-src
* attributes to src attributes, and call .load() on videos.
* @slot placeholder - the content to display before it's lazyloaded. The following properties
* are exposed to this slot:
* load {function} - function to call to load the content.
* loaded {boolean} - whether the content is loaded or not.
* modifier {string} - either '-loaded' or '' based on loaded.
*/
const loaded = ref(false)
const content = useTemplateRef('content')
const modifier = computed(() => loaded.value ? '-loaded' : '')
const load = async () => {
if (loaded.value) return
await nextTick()
const promises = []
/* Change data-src attributes to src attributes */
content.value.querySelectorAll('[data-src]')
.forEach(element => element.setAttribute('src', element.dataset.src))
/* remove inert attribute */
content.value.querySelectorAll('[inert]')
.forEach(element => element.inert = false)
/* load videos */
content.value.querySelectorAll('video')
.forEach(element => {
element.load()
promises.push(new Promise(resolve => {
element.addEventListener('loadeddata', resolve, { once: true })
}))
})
/* load iframes */
content.value.querySelectorAll('iframe')
.forEach(element => {
promises.push(new Promise(resolve => {
element.addEventListener('load', resolve, { once: true })
}))
})
/* wait for all videos and iframes to load, then update loaded.value */
Promise.all(promises).then(() => loaded.value = true)
}
</script>
<template>
<div ref="content">
<slot name="default"></slot>
</div>
<slot name="placeholder" v-bind="{ load, loaded, modifier }"></slot>
</template>
<style scoped>
div {
container-type: size;
}
</style>