Issue with Custom JavaScript Video Playback in HTML Element (First Video Works, Others Don't)

Hi all,

I’m facing a strange issue with a custom JavaScript setup I’ve implemented inside a Bubble HTML element. I’m using this approach instead of a plugin because I need complete control over video playback, including features like:

  • Manual play/pause control
  • Capturing timestamps for commenting at specific points in the video

How It Works

  • I have a list of videos uploaded by users.
  • When a user clicks on a video, I open a modal, and inside it, I render and play the video using my JavaScript (via HTML element).
  • The JavaScript initializes the video player, fetches the duration, and handles events like play, pause, etc.

The Problem

  • The first video works perfectly after page load — it plays, I get the total duration, and all controls behave as expected.
  • But when I try to open a second video (in the same modal or a different one), it fails to initialize properly.
    • No duration.
    • It doesn’t start playing until I close and reopen the modal again.
  • I initially suspected it might be a DOM cleanup issue — e.g., needing to remove event listeners or reset the video tag before loading the next one. However, even after implementing such cleanup logic, the issue persists.

What I’ve Tried

  • Removing and re-adding event listeners on modal close
  • Checking if the video element is properly appended to the DOM

Important Note on Iframe Option

One workaround I explored was enabling the “Display as an iframe” option in Bubble’s HTML element.

:white_check_mark: When I do this, the video renders and plays correctly every time.

:cross_mark: However, this approach prevents my Workflow JavaScript from accessing the video element. This breaks my custom commenting feature, which depends on full DOM access.

So, using an iframe is only a viable solution if you don’t need to interact with the video via JS.

What I’m Looking For

If anyone has dealt with a similar JavaScript-in-Bubble + modal video setup, I’d love some guidance:

  • Is there a reliable way to completely reset the HTML element or JavaScript context between modal opens?
  • Could this be related to how Bubble handles DOM lifecycle in modals?
  • Any other recommended strategies for working with multiple videos using raw JavaScript inside Bubble?

My Javacript:

<script>
  const video = document.getElementById('video');
  const timestampBar = document.getElementById('timestampBar');
  const timeDisplay = document.getElementById('timeDisplay');

  const fullscreenButton = document.getElementById('fullscreenButton');
  const playPauseButton = document.getElementById('playPauseButton');
  const rewindButton = document.getElementById('rewindButton');
  const forwardButton = document.getElementById('forwardButton');

  let isDragging = false;

  function formatTime(seconds) {
    const minutes = Math.floor(seconds / 60);
    const secs = Math.floor(seconds % 60);
    return `${minutes}:${secs < 10 ? '0' : ''}${secs}`;
  }

  function updateTimestampBar() {
    const playedPercentage = (video.currentTime / video.duration) * 100;
    timestampBar.style.background = `linear-gradient(to right, #000000 ${playedPercentage}%, #f0ebe7 ${playedPercentage}%)`;
  }

  function updateVideoTime(event) {
    const rect = timestampBar.getBoundingClientRect();
    const x = event.clientX - rect.left;
    const ratio = Math.min(Math.max(x / rect.width, 0), 1);
    video.currentTime = ratio * video.duration;
  }

  // Video Controls
  playPauseButton.addEventListener('click', () => {
    if (video.paused) {
      video.play();
      playPauseButton.src = "https://57ae69b95571dc15db44eb6c6b12a974.cdn.bubble.io/f1723118792460x382336143357890400/61180.png";
    } else {
      video.pause();
      playPauseButton.src = "https://57ae69b95571dc15db44eb6c6b12a974.cdn.bubble.io/f1723118045829x874719752319035500/play-button.svg";
    }
  });

  rewindButton.addEventListener('click', () => {
    video.currentTime = Math.max(video.currentTime - 10, 0);
  });

  forwardButton.addEventListener('click', () => {
    video.currentTime = Math.min(video.currentTime + 10, video.duration);
  });

  fullscreenButton.addEventListener('click', () => {
    if (video.requestFullscreen) {
      video.requestFullscreen();
    } else if (video.webkitRequestFullscreen) {
      video.webkitRequestFullscreen();
    } else if (video.mozRequestFullScreen) {
      video.mozRequestFullScreen();
    } else if (video.msRequestFullscreen) {
      video.msRequestFullscreen();
    }
  });

  // Timestamp scrubbing
  timestampBar.addEventListener('mousedown', (e) => {
    isDragging = true;
    updateVideoTime(e);
  });

  document.addEventListener('mousemove', (e) => {
    if (isDragging) updateVideoTime(e);
  });

  document.addEventListener('mouseup', () => {
    isDragging = false;
  });

  // Video Time Updates
  video.addEventListener('timeupdate', () => {
    updateTimestampBar();
    timeDisplay.textContent = `${formatTime(video.currentTime)} / ${formatTime(video.duration)}`;
  });

  video.addEventListener('loadedmetadata', () => {
    updateTimestampBar();
    timeDisplay.textContent = `${formatTime(0)} / ${formatTime(video.duration)}`;
  });

  // Click video to toggle play/pause
  video.addEventListener('click', () => {
    playPauseButton.click();
  });
</script>

Thanks in advance!

I haven’t delved into your issue but it does look like an issue of non unique element IDs. You’re going to have to ensure that you are generating unique element IDs for each player instance and your functions are calling the correct ones.

Thank you. But I have also tried giving video element unique ID’s, its still the same issue.

Did you adjust your script also?

const video = document.getElementById('video');

This by itself is just calling that one element ID. You’re gonna need to rewrite your script into an appropriate function to update the different elements.

Yes, I ensured the script correctly targets the active video element each time for unique IDs — verified through logging as well — but the issue still remains