Custom HTML5 Video Player with Chapters and Tooltips, No Lib

Custom native HTML, CSS and JS video player. Completely native no library is needed. Colors can be changed from CSS easily.

Enjoy using. Alternative ->

See the Pen Untitled by sinanisler (@sinanisler) on CodePen.

  <div class="video-container">
    <video id="myVideo">
      <source src="https://wpaimuse.com/wp-content/uploads/2024/03/2024-03-14-11-59-41-2.mp4" type="video/mp4">
      Your browser does not support the video tag.
    </video>
    <!-- Custom Controls -->
    <div class="controls">
      <button id="playPauseBtn">►</button>
      <div class="progress-container">
        <div id="progressBar" class="progress-bar">
          <div id="progress" class="progress"></div>
          <!-- Progress dots will be added here -->
        </div>
      </div>
      <button id="muteBtn">🔈</button>
      <button id="fullscreenBtn">⛶</button>
    </div>
    <!-- Chapter buttons -->
    <div id="chapterButtons" class="chapter-buttons" style="display:none"></div>
  </div>
    /* Container styling */
    .video-container {
      position: relative;
      width: 640px;
      margin: 0 auto;
      background: #000;
    }
    /* Video styling */
    #myVideo {
      width: 100%;
    }
    /* Custom controls styling */
    .controls {
      display: flex;
      align-items: center;
      justify-content: space-between;
      background: #333;
      padding: 10px;
      color: #fff;
    }
    .controls button {
      background: none;
      border: none;
      color: #fff;
      font-size: 20px;
      cursor: pointer;
    }
    /* Progress bar container */
    .progress-container {
      flex: 1;
      margin: 0 10px;
      position: relative;
      height: 10px;
      background: #555;
      cursor: pointer;
    }
    /* Progress bar */
    .progress-bar {
      position: relative;
      width: 100%;
      height: 100%;
    }
    .progress {
      background: #2196F3;
      height: 100%;
      width: 0%;
    }
    /* Progress dots */
    .progress-dot {
      position: absolute;
      top: 50%;
      width: 8px;
      height: 8px;
      background: #fff;
      border-radius: 50%;
      cursor: pointer;
      transform: translate(-50%, -50%);
    }
    /* Tooltip styling */
    .tooltip {
      position: absolute;
      padding: 5px 10px;
      background: rgba(0, 0, 0, 0.8);
      color: #fff;
      border-radius: 4px;
      font-size: 14px;
      white-space: nowrap;
      pointer-events: none;
      opacity: 0;
      transition: opacity 0.2s;
    }
    /* Chapter buttons styling */
    .chapter-buttons {
      margin-top: 20px;
      text-align: center;
    }
    .chapter-buttons button {
      margin: 5px;
      padding: 10px;
      font-size: 16px;
      cursor: pointer;
    }
    // Get the video element
    const video = document.getElementById('myVideo');
    // Array of chapters with title and time (in minutes:seconds)
    const chapters = [
      { title: "Chapter 1", time: "0:30" },
      { title: "Chapter 2", time: "1:00" },
      { title: "Chapter 3", time: "1:30" },
      { title: "Chapter 4", time: "2:00" }
    ];
    // Convert "min:sec" to total seconds
    function timeToSeconds(time) {
      const [minutes, seconds] = time.split(':').map(Number);
      return minutes * 60 + seconds;
    }
    // Jump to a specific time in the video
    function goToTime(seconds) {
      video.currentTime = seconds;
      video.play();
    }
    // Generate chapter buttons
    const chapterButtonsDiv = document.getElementById('chapterButtons');
    chapters.forEach((chapter) => {
      const button = document.createElement('button');
      button.textContent = `${chapter.title} - ${chapter.time}`;
      button.addEventListener('click', () => {
        const seconds = timeToSeconds(chapter.time);
        goToTime(seconds);
      });
      chapterButtonsDiv.appendChild(button);
    });
    // Custom Controls
    const playPauseBtn = document.getElementById('playPauseBtn');
    const muteBtn = document.getElementById('muteBtn');
    const fullscreenBtn = document.getElementById('fullscreenBtn');
    const progressContainer = document.querySelector('.progress-container');
    const progressBar = document.getElementById('progress');
    const progressBarContainer = document.getElementById('progressBar');
    // Tooltip element
    const tooltip = document.createElement('div');
    tooltip.className = 'tooltip';
    document.body.appendChild(tooltip);
    // Play/Pause functionality
    playPauseBtn.addEventListener('click', () => {
      if (video.paused) {
        video.play();
        playPauseBtn.textContent = '❚❚';
      } else {
        video.pause();
        playPauseBtn.textContent = '►';
      }
    });
    // Mute/Unmute functionality
    function updateMuteButton() {
      muteBtn.textContent = video.muted ? '🔈+' : '🔈-';
    }
    // Initial mute button state
    updateMuteButton();
    muteBtn.addEventListener('click', () => {
      video.muted = !video.muted;
      updateMuteButton();
    });
    // Fullscreen functionality
    fullscreenBtn.addEventListener('click', () => {
      if (video.requestFullscreen) {
        video.requestFullscreen();
      } else if (video.webkitRequestFullscreen) { /* Safari */
        video.webkitRequestFullscreen();
      } else if (video.msRequestFullscreen) { /* IE11 */
        video.msRequestFullscreen();
      }
    });
    // Update progress bar as the video plays
    video.addEventListener('timeupdate', () => {
      const percent = (video.currentTime / video.duration) * 100;
      progressBar.style.width = percent + '%';
    });
    // Seek functionality
    progressContainer.addEventListener('click', (e) => {
      const rect = progressContainer.getBoundingClientRect();
      const offsetX = e.clientX - rect.left;
      const totalWidth = rect.width;
      const percent = offsetX / totalWidth;
      video.currentTime = percent * video.duration;
    });
    // Create progress dots after metadata is loaded
    function createProgressDots() {
      chapters.forEach((chapter) => {
        const dot = document.createElement('div');
        dot.classList.add('progress-dot');
        const seconds = timeToSeconds(chapter.time);
        const percent = (seconds / video.duration) * 100;
        dot.style.left = percent + '%';
        // Store the chapter title for tooltip
        dot.dataset.title = chapter.title;
        // Add event listeners for tooltip
        dot.addEventListener('mouseenter', (e) => {
          tooltip.textContent = chapter.title;
          tooltip.style.opacity = 1;
          const rect = dot.getBoundingClientRect();
          tooltip.style.left = rect.left + 'px';
          tooltip.style.top = rect.top - 30 + 'px'; // Position above the dot
        });
        dot.addEventListener('mouseleave', () => {
          tooltip.style.opacity = 0;
        });
        dot.addEventListener('click', (e) => {
          e.stopPropagation(); // Prevent triggering the parent click event
          goToTime(seconds);
        });
        progressBarContainer.appendChild(dot);
      });
    }
    // Wait for video metadata to be loaded before creating progress dots
    if (video.readyState >= 1) {
      createProgressDots();
    } else {
      video.addEventListener('loadedmetadata', createProgressDots);
    }
    // Update Play/Pause button text based on video state
    video.addEventListener('play', () => {
      playPauseBtn.textContent = '❚❚';
    });
    video.addEventListener('pause', () => {
      playPauseBtn.textContent = '►';
    });
    // Update mute button when video mute state changes
    video.addEventListener('volumechange', updateMuteButton);

Version 2:

See the Pen Untitled by sinanisler (@sinanisler) on CodePen.