Mini Showreel Player

Category: Video & Audio. Last updated: Aug 20, 2025

Setup — External Scripts

Setup: External Scripts
html
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.1/dist/gsap.min.js"></script>
CDN 2
html
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.1/dist/Flip.min.js"></script>

Code

index.html
html
<div data-mini-showreel-lightbox="showreel" data-mini-showreel-status="not-active" class="mini-showreel-lightbox">
  <div data-mini-showreel-close="" class="mini-showreel-lightbox__dark"></div>
  <div data-mini-showreel-safearea="" class="mini-showreel-lightbox__safearea">
    <div data-mini-showreel-target="" class="mini-showreel-lightbox__target">
      <div class="mini-showreel__before"></div>
    </div>
  </div>
</div>
<div data-mini-showreel-player="showreel" data-mini-showreel-status="not-active" class="mini-showreel">
  <div class="mini-showreel__card">
    <div class="mini-showreel__media">
      <div class="mini-showreel__video">
        <div class="mini-showreel__before"></div>
        <img src="https://cdn.prod.website-files.com/69735ed659ef17a01e7fd765/69737693f36c8720e06f4cb4_wind-turbine-3000x3000.avif" loading="lazy" alt="" class="mini-showreel__cover-image">
      </div>
    </div>
    <div class="mini-showreel__info">
      <span class="mini-showreel__text">Play Showreel</span>
      <svg xmlns="http://www.w3.org/2000/svg" width="100%" viewbox="0 0 40 40" fill="none" class="mini-showreel__play-svg"><rect width="40" height="40" rx="20" fill="#6840FF"></rect><path d="M16.1206 15.8199C16.1206 14.8986 17.116 14.3209 17.916 14.778L25.1594 18.9171C25.9655 19.3778 25.9655 20.5402 25.1594 21.0009L17.916 25.14C17.116 25.5971 16.1206 25.0195 16.1206 24.0981V15.8199Z" fill="#F4F4F4"></path></svg>
    </div>
    <div data-mini-showreel-open="showreel" class="mini-showreel__click"></div>
  </div>
</div>
styles.css
css
.mini-showreel-lightbox {
  pointer-events: none;
  justify-content: center;
  align-items: center;
  padding: 3em;
  display: flex;
  position: fixed;
  inset: 0;
  overflow: hidden;
}

.mini-showreel-lightbox__safearea {
  justify-content: center;
  align-items: center;
  width: 100%;
  height: 100%;
  display: flex;
}

.mini-showreel-lightbox__target {
  justify-content: center;
  align-items: center;
  width: 100%;
  display: flex;
}

.mini-showreel-lightbox__dark {
  opacity: 0;
  pointer-events: auto;
  cursor: pointer;
  visibility: hidden;
  background-color: #0009;
  width: 100%;
  height: 100%;
  position: absolute;
}

.mini-showreel {
  flex-flow: column;
  justify-content: center;
  align-items: center;
  width: min(100vw - 3em, 25em);
  display: flex;
  position: relative;
}

.mini-showreel__card {
  grid-column-gap: 1em;
  grid-row-gap: 1em;
  color: #201d1d;
  background-color: #f4f4f4;
  border-radius: 1em;
  flex-flow: column;
  width: 100%;
  padding: 1em;
  display: flex;
  position: relative;
}

.mini-showreel__media {
  z-index: 1;
  position: relative;
}

.mini-showreel__video {
  background-color: #cfd5dc;
  border-radius: .25em;
  width: 100%;
  position: relative;
  overflow: hidden;
}

.mini-showreel__cover-image {
  object-fit: cover;
  width: 100%;
  height: 100%;
  position: absolute;
  top: 0;
  left: 0;
}

.mini-showreel__before {
  padding-top: 62.5%;
}

.mini-showreel__info {
  justify-content: space-between;
  align-items: center;
  height: 1.75em;
  padding-left: .5em;
  display: flex;
  position: relative;
}

.mini-showreel__text {
  letter-spacing: -.02em;
  font-size: 1.25em;
  font-weight: 600;
}

.mini-showreel__play-svg {
  width: 1.75em;
}

.mini-showreel__click {
  z-index: 2;
  cursor: pointer;
  width: 100%;
  height: 100%;
  position: absolute;
  top: 0;
  left: 0;
}

[data-mini-showreel-status="active"] .mini-showreel__click {
  display: none;
}

[data-mini-showreel-status] .mini-showreel__info {
  transition: margin 1s cubic-bezier(0.87, 0, 0.13, 1);
}

[data-mini-showreel-status="active"] .mini-showreel__info {
  margin-top: -2.75em;
}

[data-mini-showreel-status] .mini-showreel__card {
  transition: margin 1s cubic-bezier(0.87, 0, 0.13, 1);
}

[data-mini-showreel-status="active"] .mini-showreel__card {
  margin-top: 1.375em;
  margin-bottom: 1.375em;
}

[data-mini-showreel-status] .mini-showreel-lightbox__dark {
  transition: all 1s cubic-bezier(0.87, 0, 0.13, 1);
}

[data-mini-showreel-status="active"] .mini-showreel-lightbox__dark {
  opacity: 1;
  visibility: visible;
}

[data-mini-showreel-status="active"] .mini-showreel__click {
  display: none;
}

[data-mini-showreel-status] .mini-showreel__info {
  transition: margin 1s cubic-bezier(0.87, 0, 0.13, 1);
}

[data-mini-showreel-status="active"] .mini-showreel__info {
  margin-top: -2.75em;
}

[data-mini-showreel-status] .mini-showreel__card {
  transition: margin 1s cubic-bezier(0.87, 0, 0.13, 1);
}

[data-mini-showreel-status="active"] .mini-showreel__card {
  margin-top: 1.375em;
  margin-bottom: 1.375em;
}

[data-mini-showreel-status] .mini-showreel-lightbox__dark {
  transition: all 1s cubic-bezier(0.87, 0, 0.13, 1);
}

[data-mini-showreel-status="active"] .mini-showreel-lightbox__dark {
  opacity: 1;
  visibility: visible;
}
script.js
javascript
gsap.registerPlugin(Flip);

function initMiniShowreelPlayer() {
  const openBtns = document.querySelectorAll("[data-mini-showreel-open]");
  if (!openBtns.length) return;

  // Settings
  var duration = 1;
  var ease = "expo.inOut";
  var zIndex = 999;

  let n = "", isOpen = false;
  let lb, pw, tg;
  let pwCss = "", lbZ = "", pwZ = "";

  const q = (sel, root = document) => root.querySelector(sel);

  const getLB = (name) => q(`[data-mini-showreel-lightbox="${name}"]`);
  const getPW = (name) => q(`[data-mini-showreel-player="${name}"]`);

  const safe = (t) => t.closest("[data-mini-showreel-safearea]") || q("[data-mini-showreel-safearea]", t) || t;

  const fit = (b, a) => {
    let w = b.width, h = w / a;
    if (h > b.height) { h = b.height; w = h * a; }
    return {
      left: b.left + (b.width - w) / 2,
      top: b.top + (b.height - h) / 2,
      width: w,
      height: h
    };
  };

  const rectFor = (t) => {
    const b = safe(t).getBoundingClientRect();
    const r = t.getBoundingClientRect();
    const a = r.width > 0 && r.height > 0 ? r.width / r.height : 16 / 9;
    return fit(b, a);
  };

  const place = (el, r) =>
    gsap.set(el, {
      position: "fixed",
      left: r.left,
      top: r.top,
      width: r.width,
      height: r.height,
      margin: 0,
      x: 0,
      y: 0
    });

  function setStatus(status) {
    if (!n) return;
    document.querySelectorAll(`[data-mini-showreel-lightbox="${n}"], [data-mini-showreel-player="${n}"]`).forEach((el) => el.setAttribute("data-mini-showreel-status", status));
  }

  function zOn() {
    lbZ = lb?.style.zIndex || "";
    pwZ = pw?.style.zIndex || "";
    if (lb) lb.style.zIndex = String(zIndex);
    if (pw) pw.style.zIndex = String(zIndex);
  }

  function zOff() {
    if (lb) lb.style.zIndex = lbZ;
    if (pw) pw.style.zIndex = pwZ;
  }

  function openBy(name) {
    if (!name || isOpen) return;

    lb = getLB(name);
    pw = getPW(name);
    if (!lb || !pw) return;

    tg = q("[data-mini-showreel-target]", lb);
    if (!tg) return;

    n = name;
    isOpen = true;

    pw.dataset.flipId = n;
    pwCss = pw.style.cssText || "";

    zOn();
    setStatus("active");

    const state = Flip.getState(pw);
    place(pw, rectFor(tg));

    Flip.from(state, {
      duration: duration,
      ease: ease,
      absolute: true,
      scale: false
    });
  }

  function closeBy(nameOrEmpty) {
    if (!isOpen || !pw) return;
    if (nameOrEmpty && nameOrEmpty !== n) return;

    setStatus("not-active");

    const state = Flip.getState(pw);

    pw.style.cssText = pwCss;
    if (lb) lb.style.zIndex = String(zIndex);
    if (pw) pw.style.zIndex = String(zIndex);

    Flip.from(state, {
      duration: duration,
      ease: ease,
      absolute: true,
      scale: false,
      onComplete: () => {
        zOff();
        n = "";
        isOpen = false;
        lb = pw = tg = null;
        pwCss = "";
        lbZ = "";
        pwZ = "";
      }
    });
  }

  function onResize() {
    if (!isOpen || !pw || !tg) return;
    place(pw, rectFor(tg));
  }

  openBtns.forEach((btn) => {
    btn.addEventListener("click", (e) => {
      e.preventDefault();
      openBy(btn.getAttribute("data-mini-showreel-open") || "");
    });
  });

  document.addEventListener("click", (e) => {
    const closeBtn = e.target.closest("[data-mini-showreel-close]");
    if (!closeBtn) return;
    e.preventDefault();
    closeBy(closeBtn.getAttribute("data-mini-showreel-close") || "");
  });

  window.addEventListener("keydown", (e) => {
    if (e.key === "Escape") closeBy("");
  });

  window.addEventListener("resize", onResize);
}

// Initialize Mini Showreel Player
document.addEventListener("DOMContentLoaded", function () {
  initMiniShowreelPlayer();
});

Guide

Implementation

Lightbox

Use [data-mini-showreel-lightbox="name"] as the wrapper that contains the target frame the player should animate into.

Safearea

Use [data-mini-showreel-safearea] to constrain the maximum size of the target frame so the player always fits nicely within a defined area.

Target

Use [data-mini-showreel-target] inside the lightbox to define the exact frame the player should match in width, height, and position.

Player

Use [data-mini-showreel-player="name"] as the wrapper that gets moved and resized with Flip during open and close.

Controls

Use [data-mini-showreel-open="name"] on any trigger element to open the matching showreel and animate the player into the target. Use [data-mini-showreel-close] to close the currently open showreel, or use [data-mini-showreel-close="name"] to close a specific one.

Status

Add [data-mini-showreel-status="not-active"] on both wrapper elements so the script can switch them between active and not-active for styling and state.

Bonus: Adding the Advanced HLS Player

To keep this resource lightweight, the HLS player is optional. You can add the Custom Bunny HLS Player (Advanced) inside the card to enable a full video player experience with controls. To integrate it, follow these 3 steps.

Step 1: Add the HLS Player

Follow the resource steps from the Custom Bunny HLS Player (Advanced), on the first step replace the .mini-showreel__video element with the Webflow copy or HTML. Continue with the JavaScript and CSS part like described.

Step 2: Replace the JavaScript

Replace the initMiniShowreelPlayer() script with the updated version below that includes the HLS Player play and pause logic, so opening starts playback and closing stops playback. We still need the initBunnyPlayer() as a seperate function.

Step 3: Add extra CSS

To make sure the placeholder, dark overlay, interface and play button hide and show correctly, add the CSS below.