featureVideo.html
<div class="featureVideo accent-primary">
  <div class="featureVideo__media">
    <div class="media">
      <p-lazy>
        <video class="media__content" controls 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/960/540?random&gravity=center" alt="Video thumbnail" />
          </picture>
          <button class="media__trigger" @click="load" :class="modifier">
            <span class="srOnly" aria-label="Play video">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>
  <div class="featureVideo__background"></div>
  <div class="featureVideo__content">
    <h3 class="featureVideo__title h3">Heading Placeholder</h3>
    <p class="featureVideo__text">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque hendrerit neque a cursus tristique. Phasellus eleifend consectetur felis, in eleifend nulla vulputate eget.</p>
  </div>
</div>
index.scss
@use '@/core' as *;

.featureVideo {
  color: var(--accent-color-text);
  display: grid;
  gap: #{fluid-rems(1.5, 2)};
  padding: 0 #{fluid-rems(1.5, 2, 2.5)} #{fluid-rems(2.5, 3, 4.5)};
  position: relative;
  width: 100%;
  min-width: 0;
  grid-template-columns: 1fr;
  grid-template-rows: 1fr 1fr auto;

  > * {
    grid-column: 1;
  }

  &__media {
    display: grid;
    justify-items: center;
    width: 100%;
    min-width: 0;
    grid-row: 1 / 3;
    z-index: 2;

    .media {
      margin: 0;
      width: 100%;
      min-width: 200px;
      overflow: hidden;

      @include at(md) {
        width: min(100%, 80%);
      }
    }

    .media__mask img,
    .media__content,
    iframe {
      width: 100%;
      aspect-ratio: 16 / 9;
      object-fit: cover;
      display: block;
      box-shadow: 0 16px 64px 0 rgb(0 0 0 / 0.05);
    }
  }

  &__background {
    background: var(--accent-color);
    grid-row: 2 / -1;
    z-index: 0;
  }

  &__content {
    display: grid;
    gap: 0.75rem;
    text-align: center;
    justify-items: center;
    width: 100%;
    max-width: 42rem;
    margin: 0 auto;
    padding: 0 1rem #{fluid-rems(1.5, 3, 4)};
    align-self: start;
    grid-row: 3 / -1;
    z-index: 1;
  }

  &__title,
  &__text {
    margin: 0;
  }
}
PLazy.vue
<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" />
  </div>
  <slot name="placeholder" v-bind="{ load, loaded, modifier }" />
</template>

<style scoped>
div {
  container-type: size;
}
</style>