Custom Vimeo Lightbox (Advanced)

A fully custom Vimeo lightbox with play/pause, mute, fullscreen, timeline scrubbing, duration display, and placeholder image. Opens over a dark overlay with smooth transitions. Supports switching between multiple videos.

vimeolightboxvideoplayerfullscreen

Setup — External Scripts

Setup: External Scripts
html
<script src="https://player.vimeo.com/api/player.js"></script>

Code

index.html
html
<div class="vimeo-lightbox" data-vimeo-lightbox-init="" data-vimeo-activated="false" data-vimeo-playing="false" data-vimeo-fullscreen="false" data-vimeo-hover="false" data-vimeo-loaded="false" data-vimeo-update-size="true" data-vimeo-muted="false">
  <div data-vimeo-lightbox-control="close" class="vimeo-lightbox__bg">
    <svg class="vimeo-lightbox__loading-svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="L9" x="0px" y="0px" viewbox="0 0 100 100" enable-background="new 0 0 0 0" xml:space="preserve" width="100%"><path fill="currentColor" d="M73,50c0-12.7-10.3-23-23-23S27,37.3,27,50 M30.9,50c0-10.5,8.5-19.1,19.1-19.1S69.1,39.5,69.1,50"></path><animatetransform attributename="transform" attributetype="XML" type="rotate" dur="1s" from="0 50 50" to="360 50 50" repeatcount="indefinite"></animatetransform></svg>
  </div>
  <div class="vimeo-lightbox__calc">
    <div class="vimeo-lightbox__calc-wrap">
      <div data-vimeo-lightbox-player="" class="vimeo-lightbox__player">
        <div class="vimeo-lightbox__before"></div>
        <iframe src="" width="640" height="360" frameborder="0" allowfullscreen="true" allow="autoplay; encrypted-media" class="vimeo-lightbox__iframe"></iframe>
        <img src="https://d3e54v103j8qbb.cloudfront.net/plugins/Basic/assets/placeholder.60f9b1840c.svg" loading="lazy" alt="" class="vimeo-lightbox__placeholder">
        <div class="vimeo-lightbox__dark"></div>
        <div data-vimeo-control="play" class="vimeo-lightbox__play">
          <div class="vimeo-lightbox__btn">
            <svg xmlns="http://www.w3.org/2000/svg" width="100%" viewbox="0 0 24 24" fill="none" class="vimeo-lightbox__btn-play-svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M6.38178 6.13982C6.40762 5.94017 6.48425 5.74878 6.60555 5.58089C6.72686 5.413 6.88949 5.27326 7.08049 5.17279C7.2715 5.07232 7.48559 5.01391 7.70574 5.00219C7.92587 4.99048 8.14597 5.02579 8.3485 5.10532C9.37235 5.50436 11.6669 6.45272 14.5784 7.98469C17.4908 9.51754 19.5395 10.8562 20.4294 11.4635C21.1891 11.9829 21.191 13.013 20.4304 13.5342C19.5491 14.1381 17.5256 15.4591 14.5784 17.0113C11.6283 18.5635 9.36078 19.5005 8.34657 19.8942C7.47311 20.2343 6.49554 19.7183 6.38178 18.8597C6.24873 17.856 6 15.5769 6 12.4989C6 9.42262 6.24777 7.14443 6.38178 6.13982Z" fill="currentColor"></path></svg>
          </div>
        </div>
        <div data-vimeo-control="pause" class="vimeo-lightbox__pause">
          <div class="vimeo-lightbox__btn">
            <svg xmlns="http://www.w3.org/2000/svg" width="100%" viewbox="0 0 24 24" fill="none" class="vimeo-lightbox__btn-pause-svg"><path d="M8 6.5C8 6.22386 8.22386 6 8.5 6H10.5C10.7761 6 11 6.22386 11 6.5V17.5C11 17.7761 10.7761 18 10.5 18H8.5C8.22386 18 8 17.7761 8 17.5V6.5Z" fill="currentColor"></path><path d="M14 6.5C14 6.22386 14.2239 6 14.5 6H16.5C16.7761 6 17 6.22386 17 6.5V17.5C17 17.7761 16.7761 18 16.5 18H14.5C14.2239 18 14 17.7761 14 17.5V6.5Z" fill="currentColor"></path></svg>
          </div>
        </div>
        <div class="vimeo-lightbox__interface">
          <div class="vimeo-lightbox__interface-bottom">
            <div class="vimeo-lightbox__duration">
              <span data-vimeo-duration="" class="vimeo-lightbox__duration-span">0:00</span>
            </div>
            <div class="vimeo-lightbox__timeline">
              <progress min="0" max="100" value="0" class="vimeo-lightbox__timeline-progress"></progress>
              <input type="range" min="0" max="100" step="0.01" data-vimeo-control="timeline" class="vimeo-lightbox__timeline-input" value="0">
            </div>
            <div data-vimeo-control="mute" class="vimeo-lightbox__mute">
              <svg xmlns="http://www.w3.org/2000/svg" width="100%" viewbox="0 0 24 24" fill="none" class="vimeo-lightbox__volume-up-svg"><path d="M3 8.99998V15H7L12 20V3.99998L7 8.99998H3ZM16.5 12C16.5 10.23 15.48 8.70998 14 7.96998V16.02C15.48 15.29 16.5 13.77 16.5 12ZM14 3.22998V5.28998C16.89 6.14998 19 8.82998 19 12C19 15.17 16.89 17.85 14 18.71V20.77C18.01 19.86 21 16.28 21 12C21 7.71998 18.01 4.13998 14 3.22998Z" fill="currentColor"></path></svg>
              <svg xmlns="http://www.w3.org/2000/svg" width="100%" viewbox="0 0 24 24" fill="none" class="vimeo-lightbox__volume-mute-svg"><path d="M16.5 12C16.5 10.23 15.48 8.71 14 7.97V10.18L16.45 12.63C16.48 12.43 16.5 12.22 16.5 12ZM19 12C19 12.94 18.8 13.82 18.46 14.64L19.97 16.15C20.63 14.91 21 13.5 21 12C21 7.72 18.01 4.14 14 3.23V5.29C16.89 6.15 19 8.83 19 12ZM4.27 3L3 4.27L7.73 9H3V15H7L12 20V13.27L16.25 17.52C15.58 18.04 14.83 18.45 14 18.7V20.76C15.38 20.45 16.63 19.81 17.69 18.95L19.73 21L21 19.73L12 10.73L4.27 3ZM12 4L9.91 6.09L12 8.18V4Z" fill="currentColor"></path></svg>
            </div>
            <div class="vimeo-lightbox__fullscreen" data-vimeo-control="fullscreen">
              <svg class="vimeo-lightbox__fullscreen-scale-svg" xmlns="http://www.w3.org/2000/svg" width="100%" viewbox="0 0 24 24" fill="none"><rect x="3" y="14" width="2" height="7" fill="currentColor"></rect><rect x="3" y="3" width="2" height="7" fill="currentColor"></rect><rect x="19" y="3" width="2" height="7" fill="currentColor"></rect><rect x="19" y="14" width="2" height="7" fill="currentColor"></rect><rect x="3" y="19" width="7" height="2" fill="currentColor"></rect><rect x="14" y="19" width="7" height="2" fill="currentColor"></rect><rect x="3" y="3" width="7" height="2" fill="currentColor"></rect><rect x="14" y="3" width="7" height="2" fill="currentColor"></rect></svg>
              <svg class="vimeo-lightbox__fullscreen-shrink-svg" xmlns="http://www.w3.org/2000/svg" width="100%" viewbox="0 0 24 24" fill="none"><rect x="7" y="2" width="2" height="7" fill="currentColor"></rect><rect x="15" y="2" width="2" height="7" fill="currentColor"></rect><rect x="15" y="15" width="2" height="7" fill="currentColor"></rect><rect x="8" y="15" width="2" height="7" fill="currentColor"></rect><rect x="2" y="7" width="7" height="2" fill="currentColor"></rect><rect x="3" y="15" width="7" height="2" fill="currentColor"></rect><rect x="15" y="7" width="7" height="2" fill="currentColor"></rect><rect x="15" y="15" width="7" height="2" fill="currentColor"></rect></svg>
            </div>
          </div>
        </div>
        <div class="vimeo-lightbox__loading">
          <svg class="vimeo-lightbox__loading-svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="L9" x="0px" y="0px" viewbox="0 0 100 100" enable-background="new 0 0 0 0" xml:space="preserve" width="100%"><path fill="currentColor" d="M73,50c0-12.7-10.3-23-23-23S27,37.3,27,50 M30.9,50c0-10.5,8.5-19.1,19.1-19.1S69.1,39.5,69.1,50"></path><animatetransform attributename="transform" attributetype="XML" type="rotate" dur="1s" from="0 50 50" to="360 50 50" repeatcount="indefinite"></animatetransform></svg>
        </div>
      </div>
    </div>
  </div>
  <button data-vimeo-lightbox-control="close" class="vimeo-lightbox__close"><div class="vimeo-lightbox__close-bar"></div><div class="vimeo-lightbox__close-bar is--duplicate"></div></button>
</div>
styles.css
css
.vimeo-lightbox {
  z-index: 300;
  pointer-events: none;
  justify-content: center;
  align-items: center;
  padding: 5vw;
  display: flex;
  position: fixed;
  inset: 0;
  overflow: hidden;
}

.vimeo-lightbox__calc {
  justify-content: center;
  align-items: center;
  width: 100%;
  height: 100%;
  display: flex;
  position: relative;
}

.vimeo-lightbox__calc-wrap {
  width: 100%;
  position: relative;
}

/* Cover */
.vimeo-lightbox[data-vimeo-update-size="cover"] .vimeo-lightbox__calc-wrap {
  height: 100%;
}

.vimeo-lightbox[data-vimeo-update-size="cover"] [data-vimeo-lightbox-player] {
  width: 100%;
  min-width: 100%;
  max-width: 100%;
  height: 100%;
  min-height: 100%;
  max-height: 100%;
}

.vimeo-lightbox__player {
  pointer-events: auto;
  color: #efeeec;
  isolation: isolate;
  background-color: #131313;
  border-radius: 1em;
  justify-content: center;
  align-items: center;
  width: 100%;
  display: flex;
  position: relative;
  overflow: hidden;
  transition: all 0.3s cubic-bezier(0.625, 0.05, 0, 1);
  opacity: 0;
  visibility: hidden;
  transform: scale(0.9) rotate(0.001deg) translateX(0);
}

.vimeo-lightbox[data-vimeo-activated="true"] .vimeo-lightbox__player {
  opacity: 1;
  visibility: visible;
  transform: scale(1) rotate(0.001deg) translateX(0);
}

.vimeo-lightbox__before {
  padding-top: 62.5%;
}

/* Dark BG */
.vimeo-lightbox__bg {
  pointer-events: auto;
  background-color: #131313;
  justify-content: center;
  align-items: center;
  width: 100%;
  height: 100%;
  display: flex;
  position: absolute;
  top: 0;
  left: 0;
  transition: all 0.3s cubic-bezier(0.625, 0.05, 0, 1);
  opacity: 0;
  visibility: hidden;
}

.vimeo-lightbox[data-vimeo-activated="loading"] .vimeo-lightbox__bg,
.vimeo-lightbox[data-vimeo-activated="true"] .vimeo-lightbox__bg {
  opacity: 1;
  visibility: visible;
}

/* Close Button */
.vimeo-lightbox__close {
  z-index: 600;
  pointer-events: auto;
  border-radius: 50%;
  justify-content: center;
  align-items: center;
  width: 3em;
  height: 3em;
  display: flex;
  position: absolute;
  top: 2.5vw;
  right: 2.5vw;
  transition: all 0.5s cubic-bezier(0.625, 0.05, 0, 1);
  opacity: 0;
  visibility: hidden;
}

.vimeo-lightbox[data-vimeo-activated="true"] .vimeo-lightbox__close {
  opacity: 1;
  visibility: visible;
}

.vimeo-lightbox__close-bar {
  background-color: currentColor;
  width: 1em;
  height: .125em;
  position: absolute;
  transform: rotate(-45deg);
}

.vimeo-lightbox__close-bar.is--duplicate {
  transform: rotate(45deg);
}

.vimeo-lightbox__iframe {
  pointer-events: none;
  width: 100%;
  height: 100%;
  position: absolute;
}

/* Loading */
.vimeo-lightbox__loading {
  pointer-events: none;
  justify-content: center;
  align-items: center;
  width: 100%;
  height: 100%;
  transition: opacity .3s linear;
  display: flex;
  position: absolute;
  opacity: 0;
}

.vimeo-lightbox__loading-svg {
  color: #ff4c24;
  width: 6em;
}

.vimeo-lightbox[data-vimeo-playing="true"] .vimeo-lightbox__loading {
  opacity: 1;
}

.vimeo-lightbox[data-vimeo-playing="true"][data-vimeo-loaded="true"] .vimeo-lightbox__loading {
  opacity: 0;
}

/* Dark (Overlay) */
.vimeo-lightbox__dark {
  opacity: .5;
  pointer-events: none;
  background-color: #131313;
  width: 100%;
  height: 100%;
  transition: opacity .3s linear;
  position: absolute;
}

.vimeo-lightbox[data-vimeo-playing="false"] .vimeo-lightbox__dark {
  opacity: 0.33;
}

.vimeo-lightbox[data-vimeo-activated="false"][data-vimeo-playing="false"] .vimeo-lightbox__dark {
  opacity: 0;
}

.vimeo-lightbox[data-vimeo-activated="true"][data-vimeo-loaded="true"] .vimeo-lightbox__dark {
  opacity: 0;
}

@media (hover: hover) and (pointer: fine) {
  .vimeo-lightbox[data-vimeo-hover="true"]:hover .vimeo-lightbox__dark {
    opacity: 0.33 !important;
  }
}

/* Placeholder */
.vimeo-lightbox__placeholder {
  object-fit: cover;
  width: 100%;
  height: 100%;
  transition: opacity .3s linear;
  display: block;
  position: absolute;
}

.vimeo-lightbox[data-vimeo-loaded="true"] .vimeo-lightbox__placeholder {
  opacity: 0;
}

.vimeo-lightbox__play, .vimeo-lightbox__pause {
  cursor: pointer;
  justify-content: center;
  align-items: center;
  width: 100%;
  height: 100%;
  display: flex;
  position: absolute;
}

.vimeo-lightbox__btn {
  -webkit-backdrop-filter: blur(1em);
  backdrop-filter: blur(1em);
  background-color: #64646433;
  border-radius: 50%;
  justify-content: center;
  align-items: center;
  width: 6em;
  height: 6em;
  transition: opacity .3s linear;
  display: flex;
  position: relative;
}

.vimeo-lightbox__btn-play-svg {
  width: 40%;
}

.vimeo-lightbox__btn-pause-svg {
  width: 50%;
}

/* Pause */
.vimeo-lightbox .vimeo-lightbox__pause {
  display: none;
}

.vimeo-lightbox[data-vimeo-playing="true"] .vimeo-lightbox__pause {
  display: flex;
}

.vimeo-lightbox .vimeo-lightbox__pause .vimeo-lightbox__btn {
  opacity: 0;
}

.vimeo-lightbox[data-vimeo-activated="true"][data-vimeo-playing="false"] .vimeo-lightbox__pause .vimeo-lightbox__btn,
.vimeo-lightbox[data-vimeo-activated="true"][data-vimeo-hover="true"]:hover .vimeo-lightbox__pause .vimeo-lightbox__btn {
  opacity: 1;
}

@media (hover: none) and (pointer: coarse) {
  .vimeo-lightbox[data-vimeo-activated="true"][data-vimeo-playing="true"] .vimeo-lightbox__pause .vimeo-lightbox__btn {
    opacity: 0 !important;
  }
}

/* Play */
.vimeo-lightbox[data-vimeo-playing="true"] .vimeo-lightbox__play {
  opacity: 0;
}

/* Interface - Variables */
.vimeo-lightbox {
  --timeline-rounded-corners: 1.5em;
  --timeline-dot-height: 0.75em;
  --timeline-dot-color: #FF4C24;
  --progress-bg: rgba(239, 238, 236, 0.2);
  --progress-fill-bg: #FF4C24;
  --progress-height: 0.2em;
}

.vimeo-lightbox__interface {
  pointer-events: none;
  flex-flow: column;
  justify-content: flex-end;
  align-items: stretch;
  width: 100%;
  height: 100%;
  padding: min(2em, 4vw);
  transition-property: opacity;
  transition-duration: .3s;
  transition-timing-function: linear;
  display: flex;
  position: absolute;
  opacity: 0;
}

.vimeo-lightbox[data-vimeo-activated="false"][data-vimeo-playing="false"] .vimeo-lightbox__interface {
  opacity: 1;
}

.vimeo-lightbox .vimeo-lightbox__interface * {
  pointer-events: all;
}

.vimeo-lightbox[data-vimeo-activated="true"][data-vimeo-playing="false"] .vimeo-lightbox__interface,
.vimeo-lightbox[data-vimeo-activated="true"][data-vimeo-hover="true"]:hover .vimeo-lightbox__interface {
  opacity: 1;
}

@media (hover: none) and (pointer: coarse) {
  .vimeo-lightbox[data-vimeo-activated="true"][data-vimeo-playing="true"] .vimeo-lightbox__interface {
    opacity: 0 !important;
  }
}

.vimeo-lightbox__interface-bottom {
  grid-column-gap: 1em;
  grid-row-gap: 1em;
  justify-content: flex-start;
  align-items: center;
  display: flex;
}

/* Interface - Timeline */
.vimeo-lightbox__timeline {
  flex-grow: 1;
  justify-content: center;
  align-items: center;
  height: 1.5em;
  display: flex;
  position: relative;
}

.vimeo-lightbox__timeline-input {
  pointer-events: auto;
  cursor: pointer;
  -webkit-appearance: none;
  appearance: none;
  background-color: #0000;
  width: 100%;
  height: 100%;
  display: block;
  position: relative;
}

.vimeo-lightbox__timeline-progress {
  vertical-align: top;
  -webkit-appearance: none;
  appearance: none;
  height: var(--progress-height);
  border-radius: var(--timeline-rounded-corners);
  color: var(--progress-fill-bg);
  background-color: #0000;
  border: none;
  width: 100%;
  margin: 0;
  padding: 0;
  position: absolute;
  left: 0;
  overflow: hidden;
}

.vimeo-lightbox progress::-webkit-progress-bar {
  border-radius: var(--timeline-rounded-corners);
  background-color: var(--progress-bg);
  box-shadow: 0;
}

.vimeo-lightbox progress::-webkit-progress-value {
  background: var(--progress-fill-bg);
}

.vimeo-lightbox progress::-moz-progress-bar {
  border-radius: var(--timeline-rounded-corners);
  background: var(--progress-fill-bg);
  box-shadow: 0;
}

.vimeo-lightbox progress::-ms-fill {
  border-radius: var(--timeline-rounded-corners);
}

/* Interface - Range */
.vimeo-lightbox [type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
}

.vimeo-lightbox [type="range"]:focus {
  outline: none;
}

.vimeo-lightbox [type="range"]::-ms-track {
  width: 100%;
  cursor: pointer;
  background-color: transparent;
  border-color: transparent;
  color: transparent;
}

.vimeo-lightbox [type="range"]::-webkit-slider-runnable-track {
  width: 100%;
  height: var(--progress-height);
  cursor: pointer;
  background-color: transparent;
  border-color: transparent;
  color: transparent;
  border-radius: var(--timeline-rounded-corners);
}

.vimeo-lightbox [type="range"]::-webkit-slider-thumb {
  box-shadow: 0;
  height: var(--timeline-dot-height);
  width: var(--timeline-dot-height);
  border-radius: var(--timeline-rounded-corners);
  background-color: var(--timeline-dot-color);
  cursor: pointer;
  -webkit-appearance: none;
  margin-top: calc((var(--progress-height) / 2) - (var(--timeline-dot-height) / 2));
}

.vimeo-lightbox [type="range"]::-webkit-slider-runnable-track,
.vimeo-lightbox [type="range"]:focus::-webkit-slider-runnable-track {
  background-color: transparent;
  border-color: transparent;
  color: transparent;
}

.vimeo-lightbox [type="range"]::-moz-range-track {
  width: 100%;
  height: var(--progress-height);
  cursor: pointer;
  background: var(--progress-bg);
  border-radius: var(--timeline-rounded-corners);
  border: 0;
  overflow: hidden;
  opacity: 1 !important;
}

.vimeo-lightbox [type="range"]::-moz-range-thumb {
  box-shadow: 0;
  border: 0;
  height: var(--timeline-dot-height);
  width: var(--timeline-dot-height);
  border-radius: var(--timeline-rounded-corners);
  background: var(--timeline-dot-color);
  cursor: pointer;
}

/* Interface - Duration */
.vimeo-lightbox__duration {
  flex-shrink: 0;
  width: 2.25em;
}

.vimeo-lightbox__duration-span {
  text-align: center;
  white-space: nowrap;
  -webkit-user-select: none;
  user-select: none;
  width: 100%;
  display: block;
}

/* Interface - Mute */
.vimeo-lightbox__mute {
  cursor: pointer;
  flex-shrink: 0;
  justify-content: center;
  align-items: center;
  width: 1.5em;
  height: 1.5em;
  display: flex;
  position: relative;
}

.vimeo-lightbox__volume-up-svg {
  width: 100%;
  position: absolute;
}

.vimeo-lightbox__volume-mute-svg {
  width: 100%;
  position: absolute;
}

.vimeo-lightbox .vimeo-lightbox__mute svg:nth-child(2),
.vimeo-lightbox[data-vimeo-muted="true"] .vimeo-lightbox__mute svg:nth-child(1) {
  display: none;
}

.vimeo-lightbox .vimeo-lightbox__mute svg:nth-child(1),
.vimeo-lightbox[data-vimeo-muted="true"] .vimeo-lightbox__mute svg:nth-child(2) {
  display: block;
}

/* Interface - Fullscreen */
.vimeo-lightbox__fullscreen {
  cursor: pointer;
  justify-content: center;
  align-items: center;
  width: 1.5em;
  height: 1.5em;
  display: flex;
  position: relative;
}

.vimeo-lightbox__fullscreen-scale-svg {
  width: 100%;
  position: absolute;
}

.vimeo-lightbox__fullscreen-shrink-svg {
  width: 100%;
  position: absolute;
}

.vimeo-lightbox .vimeo-lightbox__fullscreen svg:nth-child(2),
.vimeo-lightbox[data-vimeo-fullscreen="true"] .vimeo-lightbox__fullscreen svg:nth-child(1) {
  display: none;
}

.vimeo-lightbox .vimeo-lightbox__fullscreen svg:nth-child(1),
.vimeo-lightbox[data-vimeo-fullscreen="true"] .vimeo-lightbox__fullscreen svg:nth-child(2) {
  display: block;
}
script.js
javascript
function initVimeoLightboxAdvanced() {
  const lightbox = document.querySelector('[data-vimeo-lightbox-init]');
  if (!lightbox) return;

  const openButtons  = document.querySelectorAll('[data-vimeo-lightbox-control="open"]');
  const closeButtons = document.querySelectorAll('[data-vimeo-lightbox-control="close"]');

  let iframe            = lightbox.querySelector('iframe');
  const placeholder     = lightbox.querySelector('.vimeo-lightbox__placeholder');
  const calcEl          = lightbox.querySelector('.vimeo-lightbox__calc');
  const wrapEl          = lightbox.querySelector('.vimeo-lightbox__calc-wrap');
  const playerContainer = lightbox.querySelector('[data-vimeo-lightbox-player]');

  let player = null;
  let currentVideoID = null;
  let videoAspectRatio = null;
  let globalMuted = lightbox.getAttribute('data-vimeo-muted') === 'true';
  const isTouch = ('ontouchstart' in window) || navigator.maxTouchPoints > 0;
  const playedOnce = new Set();

  function formatTime(s) {
    const m   = Math.floor(s / 60);
    const sec = Math.floor(s % 60).toString().padStart(2, '0');
    return `${m}:${sec}`;
  }

  function clampWrapSize(ar) {
    const w = calcEl.offsetWidth;
    const h = calcEl.offsetHeight;
    wrapEl.style.maxWidth = Math.min(w, h / ar) + 'px';
  }

  function adjustCoverSizing() {
    if (!videoAspectRatio) return;
    const cH = playerContainer.offsetHeight;
    const cW = playerContainer.offsetWidth;
    const r  = cH / cW;
    const wEl = lightbox.querySelector('.vimeo-lightbox__iframe');
    if (r > videoAspectRatio) {
      wEl.style.width  = (r / videoAspectRatio * 100) + '%';
      wEl.style.height = '100%';
    } else {
      wEl.style.height = (videoAspectRatio / r * 100) + '%';
      wEl.style.width  = '100%';
    }
  }

  function closeLightbox() {
    lightbox.setAttribute('data-vimeo-activated', 'false');
    if (player) {
      player.pause();
      lightbox.setAttribute('data-vimeo-playing', 'false');
    }
  }

  document.addEventListener('keydown', e => {
    if (e.key === 'Escape') closeLightbox();
  });
  closeButtons.forEach(btn => btn.addEventListener('click', closeLightbox));

  function setupPlayerEvents() {
    player.on('play', () => {
      lightbox.setAttribute('data-vimeo-loaded', 'true');
      lightbox.setAttribute('data-vimeo-playing', 'true');
    });
    player.on('ended', closeLightbox);
    player.on('pause', () => {
      lightbox.setAttribute('data-vimeo-playing', 'false');
    });

    const durEl = lightbox.querySelector('[data-vimeo-duration]');
    player.getDuration().then(d => {
      if (durEl) durEl.textContent = formatTime(d);
      lightbox.querySelectorAll('[data-vimeo-control="timeline"],progress')
        .forEach(el => el.max = d);
    });

    const tl = lightbox.querySelector('[data-vimeo-control="timeline"]');
    const pr = lightbox.querySelector('progress');
    player.on('timeupdate', data => {
      if (tl) tl.value = data.seconds;
      if (pr) pr.value = data.seconds;
      if (durEl) durEl.textContent = formatTime(Math.trunc(data.seconds));
    });
    if (tl) {
      ['input','change'].forEach(evt =>
        tl.addEventListener(evt, e => {
          const v = e.target.value;
          player.setCurrentTime(v);
          if (pr) pr.value = v;
        })
      );
    }

    let hoverTimer;
    playerContainer.addEventListener('mousemove', () => {
      lightbox.setAttribute('data-vimeo-hover', 'true');
      clearTimeout(hoverTimer);
      hoverTimer = setTimeout(() => {
        lightbox.setAttribute('data-vimeo-hover', 'false');
      }, 3000);
    });

    const fsBtn = lightbox.querySelector('[data-vimeo-control="fullscreen"]');
    if (fsBtn) {
      const isFS = () => document.fullscreenElement || document.webkitFullscreenElement;
      if (!(document.fullscreenEnabled || document.webkitFullscreenEnabled)) {
        fsBtn.style.display = 'none';
      }
      fsBtn.addEventListener('click', () => {
        if (isFS()) {
          lightbox.setAttribute('data-vimeo-fullscreen', 'false');
          (document.exitFullscreen || document.webkitExitFullscreen).call(document);
        } else {
          lightbox.setAttribute('data-vimeo-fullscreen', 'true');
          (playerContainer.requestFullscreen || playerContainer.webkitRequestFullscreen)
            .call(playerContainer);
        }
      });
      ['fullscreenchange','webkitfullscreenchange'].forEach(evt =>
        document.addEventListener(evt, () =>
          lightbox.setAttribute('data-vimeo-fullscreen', isFS() ? 'true' : 'false')
        )
      );
    }
  }

  async function runSizing() {
    const mode = lightbox.getAttribute('data-vimeo-update-size');
    const w    = await player.getVideoWidth();
    const h    = await player.getVideoHeight();
    const ar   = h / w;
    const bef  = lightbox.querySelector('.vimeo-lightbox__before');

    if (mode === 'true') {
      if (bef) bef.style.paddingTop = (ar * 100) + '%';
      clampWrapSize(ar);
    } else if (mode === 'cover') {
      videoAspectRatio = ar;
      if (bef) bef.style.paddingTop = '0%';
      adjustCoverSizing();
    } else {
      clampWrapSize(ar);
    }
  }

  window.addEventListener('resize', () => {
    if (player) runSizing();
  });

  async function openLightbox(id, placeholderBtn) {
    lightbox.setAttribute('data-vimeo-activated', 'loading');
    lightbox.setAttribute('data-vimeo-loaded',    'false');

    if (player && id !== currentVideoID) {
      await player.pause();
      await player.unload();

      const oldIframe = iframe;
      const newIframe = document.createElement('iframe');
      newIframe.className = oldIframe.className;
      newIframe.setAttribute('frameborder', '0');
      newIframe.setAttribute('allowfullscreen', 'true');
      newIframe.setAttribute('allow', 'autoplay; encrypted-media');
      oldIframe.parentNode.replaceChild(newIframe, oldIframe);

      iframe         = newIframe;
      player         = null;
      currentVideoID = null;
      lightbox.setAttribute('data-vimeo-playing', 'false');
    }

    if (placeholderBtn) {
      ['src','srcset','sizes','alt','width'].forEach(attr => {
        const val = placeholderBtn.getAttribute(attr);
        if (val != null) placeholder.setAttribute(attr, val);
      });
    }

    if (!player) {
      iframe.src = `https://player.vimeo.com/video/${id}?api=1&background=1&autoplay=0&loop=0&muted=0`;
      player = new Vimeo.Player(iframe);
      setupPlayerEvents();
      currentVideoID = id;
      await runSizing();
    }

    lightbox.setAttribute('data-vimeo-activated', 'true');

    if (!isTouch) {
      player.setVolume(globalMuted ? 0 : 1).then(() => {
        lightbox.setAttribute('data-vimeo-playing', 'true');
        setTimeout(() => player.play(), 50);
      });
    } else if (playedOnce.has(currentVideoID)) {
      player.setVolume(globalMuted ? 0 : 1).then(() => {
        lightbox.setAttribute('data-vimeo-playing', 'true');
        player.play();
      });
    }
  }

  lightbox.querySelector('[data-vimeo-control="play"]').addEventListener('click', () => {
    if (isTouch) {
      if (!playedOnce.has(currentVideoID)) {
        player.setVolume(0).then(() => {
          lightbox.setAttribute('data-vimeo-playing', 'true');
          player.play();
          if (!globalMuted) {
            setTimeout(() => {
              player.setVolume(1);
              lightbox.setAttribute('data-vimeo-muted', 'false');
            }, 100);
          }
          playedOnce.add(currentVideoID);
        });
      } else {
        player.setVolume(globalMuted ? 0 : 1).then(() => {
          lightbox.setAttribute('data-vimeo-playing', 'true');
          player.play();
        });
      }
    } else {
      player.setVolume(globalMuted ? 0 : 1).then(() => {
        lightbox.setAttribute('data-vimeo-playing', 'true');
        setTimeout(() => player.play(), 50);
      });
    }
  });

  lightbox.querySelector('[data-vimeo-control="pause"]').addEventListener('click', () => {
    player.pause();
  });

  lightbox.querySelector('[data-vimeo-control="mute"]').addEventListener('click', () => {
    globalMuted = !globalMuted;
    player.setVolume(globalMuted ? 0 : 1).then(() =>
      lightbox.setAttribute('data-vimeo-muted', globalMuted ? 'true' : 'false')
    );
  });

  openButtons.forEach(btn => {
    btn.addEventListener('click', () => {
      const vid = btn.getAttribute('data-vimeo-lightbox-id');
      const img = btn.querySelector('[data-vimeo-lightbox-placeholder]');
      openLightbox(vid, img);
    });
  });
}

// Initialize Vimeo Lightbox (Advanced)
document.addEventListener('DOMContentLoaded', function() {
  initVimeoLightboxAdvanced();
});

Attributes

NameTypeDefaultDescription
data-vimeo-lightbox-initbooleanMarks the lightbox root element. The script queries this to initialise the player.
data-vimeo-lightbox-controlstringAdd to any clickable element. Use "open" to open the lightbox and "close" to close it.
data-vimeo-lightbox-idstringThe numeric Vimeo video ID (e.g. 1019191082). Add to the open button. The video must allow embedding in Vimeo settings.
data-vimeo-lightbox-placeholderbooleanAdd to an <img> inside the open button to provide a placeholder image. Can be hidden with display: none — used as data only.
data-vimeo-activatedstringfalseReflects the lightbox open state. Values: false (closed), loading (fetching video info), true (open and ready).
data-vimeo-playingbooleanfalseReflects playback state. true while playing, false when paused or stopped. Updated automatically.
data-vimeo-loadedbooleanfalseSet to true once the video starts playing. Use to hide the loading spinner and placeholder.
data-vimeo-mutedbooleanfalseReflects and controls the mute state. Set to "true" to start muted. Updated automatically when toggled.
data-vimeo-fullscreenbooleanfalseReflects fullscreen state. Updated automatically. If fullscreen is not supported, the fullscreen button is hidden.
data-vimeo-hoverbooleanfalseSet to true while the mouse is moving over the player. Resets to false after 3 seconds of inactivity. Use to show/hide controls.
data-vimeo-update-sizestringtrueControls aspect ratio behaviour. "true" calculates the ratio from the video and applies padding-top. "cover" scales the iframe to fill the container.
data-vimeo-controlstringAdd to UI elements inside the player. Accepted values: play, pause, mute, fullscreen, timeline.
data-vimeo-durationbooleanDisplays the video duration on load, then updates to show elapsed time during playback.
data-vimeo-lightbox-playerbooleanMarks the inner player container. Used for sizing calculations and fullscreen requests.

Notes

  • The Vimeo video must have embedding allowed in its Vimeo privacy settings.
  • The Vimeo Player API script must be loaded before the lightbox script runs.
  • On desktop, the video starts playing automatically when the lightbox opens. On touch devices, the user must tap play first.
  • When switching to a different video, the old iframe is replaced with a fresh one to ensure a clean player state.
  • Pressing Escape always closes the lightbox.
  • If fullscreen is not supported by the browser or device, the fullscreen button is automatically hidden.

Guide

Open the lightbox

Add [data-vimeo-lightbox-control="open"] to any element. Set the video ID on [data-vimeo-lightbox-id] — this is the numeric ID from the Vimeo URL (e.g. 1019191082). To show a placeholder image while loading, add an <img> with [data-vimeo-lightbox-placeholder] as a child of the open button — it can be hidden with display: none.

<button data-vimeo-lightbox-control="open" data-vimeo-lightbox-id="1019191082">
  <img data-vimeo-lightbox-placeholder="" src="image.jpg" alt="">
  <span>Open Video</span>
</button>

Close the lightbox

Add [data-vimeo-lightbox-control="close"] to any element. Clicking it pauses the video and closes the lightbox. Pressing the Escape key also closes it.

<button data-vimeo-lightbox-control="close">Close Video</button>

Activated state

When the lightbox opens it sets [data-vimeo-activated="loading"], which shows the dark background immediately via CSS. Once the video information is fetched it changes to [data-vimeo-activated="true"] and reveals the player.

Update size (aspect ratio)

Set [data-vimeo-update-size="true"] to have the script retrieve the video's dimensions and update .vimeo-lightbox__before with the correct padding-top percentage (e.g. 56.25% for 16:9). Set it to "cover" to make the iframe fill its parent container — add position: relative to the parent.

Muted

Set [data-vimeo-muted="true"] on the lightbox element to start the video muted. The user can toggle the mute state by clicking an element with [data-vimeo-control="mute"].

Play / Pause

Add [data-vimeo-control="play"] and [data-vimeo-control="pause"] to elements inside the player. These update [data-vimeo-playing] automatically, which you can use in CSS to show or hide each button.

Fullscreen

Add [data-vimeo-control="fullscreen"] to an element to toggle fullscreen. [data-vimeo-fullscreen] is updated automatically. On devices or browsers that do not support fullscreen, the button is hidden automatically.

Loading

While the video is loading, .vimeo-lightbox__loading is visible. Once [data-vimeo-loaded="true"] is set (on first play), you can hide the loader and the placeholder via CSS.

Timeline

The timeline uses an <input type="range"> with [data-vimeo-control="timeline"] for scrubbing, paired with a <progress> element for the filled bar. Both are updated automatically during playback.

Duration

Add [data-vimeo-duration] to a text element. It shows the total duration on load and updates to the elapsed time during playback.

Hover

[data-vimeo-hover="true"] is set when the mouse moves over the player and resets to false after 3 seconds of no movement. Use it in CSS to show or hide the interface while the video plays.