media.html
<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="" width="320" height="176" />
      </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="" width="320" height="176" />
      </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>
  </p-lazy>
</div>

<br />

<div class="media">
  <p-lazy>
    <iframe data-src="https://player.vimeo.com/video/818944000?h=e8674039c8&autoplay=1&title=0&byline=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="" width="267" height="150" />
      </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>
index.scss
.media {
  $b: &;

  background-color: rgb(from currentcolor r g b / 0.25);
  display: inline grid;
  margin-bottom: 2rem;
  width: 267px;
  height: 150px;
  aspect-ratio: 267 / 150;
  overflow: hidden;

  @media (width >= 768px) {
    width: 300px;
    aspect-ratio: 300 / 150;
  }

  > * {
    grid-area: 1 / 1;
    position: relative;
  }

  /* Ensure wrapper divs from components fill the grid cell */
  > div:not(.media__mask, .media__trigger) {
    width: 100%;
    height: 100%;
    min-width: 0;
    min-height: 0;
    overflow: hidden;
  }

  /* Ensure iframes and videos don't expand their containers */
  iframe,
  video {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    border: 0;
    max-width: 100%;
    max-height: 100%;
    object-fit: contain;
  }

  &__mask {
    margin: 0;
    display: inline-block;
    transition: opacity var(--root-ease-out-moderate);
    line-height: 0;
    vertical-align: top;

    img {
      display: block;
      width: 100%;
    }

    &.-loaded {
      opacity: 0;
      pointer-events: none;
    }
  }

  &__maskImg {
    width: 100%;
    height: 100%;
    object-fit: cover;
  }

  &__trigger {
    appearance: none;
    background: #00000000;
    border: 0;
    border-radius: max(50cqmin, 6rem);
    padding: 0;
    place-self: center 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 {
      place-self: start end;
      height: min(25cqmin, 3rem);
      justify-self: end;
      margin: 0.5rem;
      opacity: 0;
      pointer-events: none;
      transition: opacity var(--root-ease-out-fast);
      width: min(25cqmin, 3rem);

      &.-keepVisible {
        opacity: 0.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, 0.75rem);
      transition: padding var(--root-ease-out-fast);

      &:hover {
        padding: min(4cqmin, 0.5rem);
      }
    }
  }

  /**
   * hacks to target and better style iframes from youtube/vimeo embeds from CKEditor.
   */
  &:has(> iframe):not(:has(p-wistia), :has(p-lazy), :has(p-you-tube-playlist)) {
    display: grid;

    /* these divs are using a padding-bottom hack for the aspect ratio. */
    div[style]:has(> iframe) {
      position: relative;

      > iframe {
        position: absolute;
        inset: 0;
      }
    }
  }

  /* Ensure p-lazy wrapper div matches image size */
  &:has(p-lazy) {
    > div:first-child {
      position: absolute;
      inset: 0;
      width: 100%;
      height: 100%;
      overflow: hidden;
    }

    > div:first-child iframe,
    > div:first-child video {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      max-width: 100%;
      max-height: 100%;
    }
  }

  /* Ensure p-you-tube-playlist wrapper div contains the iframe */
  &:has(p-you-tube-playlist) {
    display: block;
    height: 150px;
    width: 267px;

    @media (width >= 768px) {
      width: 300px;
    }

    .p-youtube-playlist-wrapper {
      width: 100%;
      height: 150px;
      min-height: 150px;
      position: relative;
      overflow: visible;
    }

    .p-youtube-playlist-wrapper iframe {
      position: relative;
      width: 100%;
      height: 150px;
      min-height: 150px;
      display: block;
      top: auto;
      left: auto;
      max-width: none;
      max-height: none;
      object-fit: none;
    }
  }
}
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>