<!-- Designed by:  Mauricio Bucardo
Original image: https://dribbble.com/shots/6957353-Music-Player-Widget -->

<div id="root"></div>
@font-face {
  font-family: "icomoon";
  src: url("https://raw.githubusercontent.com/abxlfazl/music-player-widget/main/src/assets/icomoon/fonts/icomoon.eot?u8ckod");
  src: url("https://raw.githubusercontent.com/abxlfazl/music-player-widget/main/src/assets/icomoon/fonts/icomoon.eot?u8ckod#iefix")
      format("embedded-opentype"),
    url("https://raw.githubusercontent.com/abxlfazl/music-player-widget/main/src/assets/icomoon/fonts/icomoon.ttf?u8ckod")
      format("truetype"),
    url("https://raw.githubusercontent.com/abxlfazl/music-player-widget/main/src/assets/icomoon/fonts/icomoon.woff?u8ckod")
      format("woff"),
    url("https://raw.githubusercontent.com/abxlfazl/music-player-widget/main/src/assets/icomoon/fonts/icomoon.svg?u8ckod#icomoon")
      format("svg");
  font-weight: normal;
  font-style: normal;
  font-display: block;
}

[class^="icon-"],
[class*=" icon-"] {
  /* use !important to prevent issues with browser extensions that change fonts */
  font-family: "icomoon" !important;
  speak: never;
  font-style: normal;
  font-weight: normal;
  font-variant: normal;
  text-transform: none;
  line-height: 1;

  /* Better Font Rendering =========== */
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

.icon-back:before {
  content: "\e900";
  color: #827d7b;
}
.icon-next:before {
  content: "\e901";
  color: #827d7b;
}
.icon-pause:before {
  content: "\e902";
  color: #fff;
}
.icon-play:before {
  content: "\e903";
  color: #fff;
}
.icon-playlist:before {
  content: "\e904";
  color: #fff;
}

@font-face {
  font-family: Avenir;
  src: url(https://raw.githubusercontent.com/abxlfazl/music-player-widget/main/src/assets/font/AvenirNextRoundedProMedium.TTF);
}

html {
  box-sizing: border-box;

  --duration: 1s;
  --ease-slider: cubic-bezier(0.4, 0, 0.2, 1);
  --ease-timeline: cubic-bezier(0.71, 0.21, 0.3, 0.95);
}
html *,
html *::before,
html *::after {
  box-sizing: inherit;
  scrollbar-width: none;
}
body {
  margin: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  height: calc(var(--vH) * 100);
  font-family: Avenir, sans-serif;
  background-color: var(--body-bg, #fff);
  -webkit-tap-highlight-color: transparent;
  transition: var(--duration) background-color var(--ease-slider);
}
::-webkit-scrollbar {
  width: 0;
  height: 0;
}

/* PUBLIC CLASSES */

.img {
  width: 100%;
  flex-shrink: 0;
  display: block;
  object-fit: cover;
}
.list {
  margin: 0;
  padding: 0;
  list-style-type: none;
}
.text_trsf-cap {
  text-transform: capitalize;
}
.button {
  all: unset;
  cursor: pointer;
}
.center {
  display: flex;
  align-items: center;
  justify-content: center;
}
.flex-row {
  display: flex;
}
.flex-column {
  display: flex;
  flex-direction: column;
}
._align_center {
  align-items: center;
}
._align_start {
  align-items: flex-start;
}
._align_end {
  align-items: flex-end;
}
._justify_center {
  justify-content: center;
}
._justify_start {
  justify-content: flex-start;
}
._justify_end {
  justify-content: flex-end;
}
._justify_space-btwn {
  justify-content: space-between;
}
.text_overflow {
  width: 66%;
  overflow: hidden;
  white-space: nowrap;
  display: inline-block;
  text-overflow: ellipsis;
}
.loading {
  gap: 0 0.5rem;
  font-size: 5rem;
  font-weight: bold;
}

/* PUBLIC CLASSES */

.music-player {
  --color-white: #fff;
  --color-gray: #e5e7ea;
  --color-blue: #78adfe;
  --color-blue-dark: #5781bd;

  --box-shadow: 0 2px 6px 1px #0000001f;

  --color-text-1: #000;
  --color-text-2: #0000006b;

  --cover-size: 3.8125em;
  --border-radius: 1.625em;

  --music-player-height: 24.375em;
  --offset-cover: 1.60125em;

  width: 20.9375em;
  overflow: hidden;
  user-select: none;
  color: var(--color-text-1);
  height: var(--music-player-height);
  border-radius: var(--border-radius);
  background-color: var(--color-white);
}
.slider {
  --shadow-opacity: 1;

  z-index: 0;
  flex-shrink: 0;
  height: 7.125em;
  position: relative;
  border-radius: inherit;
  transition: var(--duration) height var(--ease-timeline);
}
.slider.resize {
  --shadow-opacity: 0;

  height: var(--music-player-height);
}
.slider::after {
  top: 0;
  left: 0;
  right: 0;
  content: "";
  width: 100%;
  z-index: -1;
  height: 100%;
  position: absolute;
  pointer-events: none;
  border-radius: inherit;
  box-shadow: var(--box-shadow);
  opacity: var(--shadow-opacity);
  transition: var(--duration) opacity;
}
.slider__content {
  top: 0;
  left: 0;
  overflow: hidden;
  position: absolute;
  border-radius: inherit;
  width: var(--cover-size);
  height: var(--cover-size);
  transition: transform, width, height;
  transition-duration: var(--duration);
  transition-timing-function: var(--ease-timeline);
  transform: translate3d(var(--offset-cover), var(--offset-cover), 0);
}
.slider.resize .slider__content {
  width: 100%;
  height: 17.8125em;
  transform: translate3d(0, 0, 0);
}
.slider__content .button {
  --size: 3em;

  z-index: 1;
  position: absolute;
  width: var(--size);
  height: var(--size);
}
.slider__content i {
  position: absolute;
  pointer-events: none;
  font-size: var(--size);
}
.music-player__playlist-button {
  top: 5.5%;
  left: 5.5%;
  transform: scale(0);
  transition: calc(var(--duration) / 2) transform;
}
.slider.resize .music-player__playlist-button {
  transform: scale(1);
  transition: 0.35s var(--duration) transform cubic-bezier(0, 0.85, 0.11, 1.64);
}
.music-player__broadcast-guarantor .icon-pause,
.music-player__broadcast-guarantor.click .icon-play {
  opacity: 0;
}
.music-player__broadcast-guarantor.click .icon-pause {
  opacity: 1;
}
.slider__imgs {
  width: 100%;
  height: 100%;
  filter: brightness(75%);
  transform: translate3d(calc(var(--index) * 100%), 0, 0);
  transition: var(--duration) transform var(--ease-slider);
}
.slider__imgs > img {
  pointer-events: none;
}
.slider__controls {
  --controls-y: 145%;
  --controls-x: 17.3%;
  --controls-width: 68.4%;
  --controls-resize-width: 88%;
  /* Animation performance is better than transition */

  gap: 0.375em 0;
  flex-wrap: wrap;
  position: absolute;
  align-items: center;
  padding-top: 0.375em;
  width: var(--controls-width);
  transform: translate3d(var(--controls-x), 0, 0);
  animation: var(--controls-animate, "down paused") var(--duration)
    var(--ease-timeline) forwards;
}
@keyframes down {
  100% {
    width: var(--controls-resize-width);
    transform: translate3d(0, var(--controls-y), 0);
  }
}
@keyframes up {
  0% {
    width: var(--controls-resize-width);
    transform: translate3d(0, var(--controls-y), 0);
  }
  100% {
    width: var(--controls-width);
    transform: translate3d(var(--controls-x), 0, 0);
  }
}
.slider__switch-button {
  font-size: 3em;
  height: max-content;
}
.music-player__info {
  width: 56.3%;
  cursor: pointer;
  line-height: 1.8;
  overflow: hidden;
  font-weight: bold;
  padding: 0 0.0625em;
  white-space: nowrap;
}
.music-player__info > * {
  margin: 0 auto;
  pointer-events: none;
}
.music-player__singer-name {
  font-size: 1.25em;
  width: max-content;
}
.music-player__subtitle {
  font-size: 0.85em;
  font-weight: bold;
  color: var(--color-text-2);
}
.slider__controls .music-player__subtitle {
  width: max-content;
}
.music-player__singer-name.animate,
.music-player__subtitle.animate {
  --subtitle-gap: 1.5625em;

  display: flex;
  gap: 0 var(--subtitle-gap);
  animation: subtitle 12s 1.2s linear infinite;
}
@keyframes subtitle {
  80%,
  100% {
    transform: translate3d(calc((100% + var(--subtitle-gap)) / -2), 0, 0);
  }
}
.progress {
  width: 90%;
  height: 1.25em;
  cursor: pointer;
  transition: var(--duration) width var(--ease-timeline);
}
.slider.resize .progress {
  width: 100%;
}
.progress__wrapper {
  width: 100%;
  height: 0.3125em;
  position: relative;
  border-radius: 1em;
  background-color: var(--color-gray);
}
.progress__bar {
  top: 0;
  left: 0;
  bottom: 0;
  position: absolute;
  width: var(--width);
  border-radius: inherit;
  background-color: var(--color-blue);
}
.progress__bar::after {
  --size: 0.4375em;

  left: 98%;
  content: "";
  position: absolute;
  width: var(--size);
  height: var(--size);
  border-radius: 100%;
  background-color: var(--color-blue-dark);
}
.music-player__playlist {
  height: 100%;
  overflow: hidden auto;
  padding: 1.28125em 1.09375em 0 var(--offset-cover);
}
.music-player__song {
  --gap: 0.75em;

  cursor: pointer;
  margin-bottom: var(--gap);
  padding-bottom: var(--gap);
  border-bottom: 1.938px solid #d8d8d859;
}
.music-player__song audio {
  display: none;
}
.music-player__song-img {
  width: var(--cover-size);
  height: var(--cover-size);
  border-radius: var(--border-radius);
}
.music-player__playlist-info {
  width: 100%;
  overflow: hidden;
  line-height: 1.3;
  font-size: 1.06875em;
  margin-left: 0.7875em;
}
.music-player__song-duration {
  font-weight: bold;
  font-size: 0.7875em;
  color: var(--color-text-2);
}

@media screen and (min-width: 1366px) {
  .music-player {
    font-size: 1.17132vw;
  }
}
@media screen and (max-width: 480px) {
  .music-player {
    font-size: 0.8rem;
  }
}
@media screen and (max-width: 280px) {
  .music-player {
    font-size: 0.6rem;
  }
}
/** @jsx dom */

let indexSong = 0;
let isLocked = false;
let songsLength = null;
let selectedSong = null;
let loadingProgress = 0;
let songIsPlayed = false;
let progress_elmnt = null;
let songName_elmnt = null;
let sliderImgs_elmnt = null;
let singerName_elmnt = null;
let progressBar_elmnt = null;
let playlistSongs_elmnt = [];
let loadingProgress_elmnt = null;
let musicPlayerInfo_elmnt = null;
let progressBarIsUpdating = false;
let broadcastGuarantor_elmnt = null;
const root = querySelector("#root");

function App({ songs }) {
  function handleChangeMusic({ isPrev = false, playListIndex = null }) {
    if (isLocked || indexSong === playListIndex) return;

    if (playListIndex || playListIndex === 0) {
      indexSong = playListIndex;
    } else {
      indexSong = isPrev ? (indexSong -= 1) : (indexSong += 1);
    }

    if (indexSong < 0) {
      indexSong = 0;
      return;
    } else if (indexSong > songsLength) {
      indexSong = songsLength;
      return;
    }

    selectedSong.pause();
    selectedSong.currentTime = 0;
    progressBarIsUpdating = false;
    selectedSong = playlistSongs_elmnt[indexSong];

    if (selectedSong.paused && songIsPlayed) selectedSong.play();
    else selectedSong.pause();

    setBodyBg(songs[indexSong].bg);
    setProperty(sliderImgs_elmnt, "--index", -indexSong);
    updateInfo(singerName_elmnt, songs[indexSong].artist);
    updateInfo(songName_elmnt, songs[indexSong].songName);
  }

  setBodyBg(songs[0].bg);

  return (
    <div class="music-player flex-column">
      <Slider slides={songs} handleChangeMusic={handleChangeMusic} />
      <Playlist list={songs} handleChangeMusic={handleChangeMusic} />
    </div>
  );
}

function Slider({ slides, handleChangeMusic }) {
  function handleResizeSlider({ target }) {
    if (isLocked) {
      return;
    } else if (target.classList.contains("music-player__info")) {
      this.classList.add("resize");
      setProperty(this, "--controls-animate", "down running");
      return;
    } else if (target.classList.contains("music-player__playlist-button")) {
      this.classList.remove("resize");
      setProperty(this, "--controls-animate", "up running");
      return;
    }
  }
  function handlePlayMusic() {
    if (selectedSong.currentTime === selectedSong.duration) {
      handleChangeMusic({});
    }

    this.classList.toggle("click");
    songIsPlayed = !songIsPlayed;
    selectedSong.paused ? selectedSong.play() : selectedSong.pause();
  }

  return (
    <div class="slider center" onClick={handleResizeSlider}>
      <div class="slider__content center">
        <button class="music-player__playlist-button center button">
          <i class="icon-playlist" />
        </button>
        <button
          onClick={handlePlayMusic}
          class="music-player__broadcast-guarantor center button"
        >
          <i class="icon-play" />
          <i class="icon-pause" />
        </button>
        <div class="slider__imgs flex-row">
          {slides.map(({ songName, files: { cover } }) => (
            <img src={cover} class="img" alt={songName} />
          ))}
        </div>
      </div>
      <div class="slider__controls center">
        <button
          class="slider__switch-button flex-row button"
          onClick={() => handleChangeMusic({ isPrev: true })}
        >
          <i class="icon-back" />
        </button>
        <div class="music-player__info text_trsf-cap">
          <div>
            <div class="music-player__singer-name">
              <div>{slides[0].artist}</div>
            </div>
          </div>
          <div>
            <div class="music-player__subtitle">
              <div>{slides[0].songName}</div>
            </div>
          </div>
        </div>
        <button
          class="slider__switch-button flex-row button"
          onClick={() => handleChangeMusic({ isPrev: false })}
        >
          <i class="icon-next" />
        </button>
        <div
          class="progress center"
          onPointerdown={(e) => {
            handleScrub(e);
            progressBarIsUpdating = true;
          }}
        >
          <div class="progress__wrapper">
            <div class="progress__bar center" />
          </div>
        </div>
      </div>
    </div>
  );
}

function Playlist({ list, handleChangeMusic }) {
  function loadedAudio() {
    const duration = this.duration;
    const target = this.parentElement.querySelector(
      ".music-player__song-duration"
    );

    let min = parseInt(duration / 60);
    if (min < 10) min = "0" + min;

    let sec = parseInt(duration % 60);
    if (sec < 10) sec = "0" + sec;

    target.appendChild(document.createTextNode(`${min}:${sec}`));
  }

  function updateTheProgressBar() {
    const duration = this.duration;
    const currentTime = this.currentTime;

    const progressBarWidth = (currentTime / duration) * 100;
    setProperty(progressBar_elmnt, "--width", `${progressBarWidth}%`);

    if (songIsPlayed && currentTime === duration) {
      handleChangeMusic({});
    }

    if (
      indexSong === songsLength &&
      this === selectedSong &&
      currentTime === duration
    ) {
      songIsPlayed = false;
      broadcastGuarantor_elmnt.classList.remove("click");
    }
  }

  return (
    <ul class="music-player__playlist list">
      {list.map(({ songName, artist, files: { cover, song } }, index) => {
        return (
          <li
            class="music-player__song"
            onClick={() =>
              handleChangeMusic({ isPrev: false, playListIndex: index })
            }
          >
            <div class="flex-row _align_center">
              <img src={cover} class="img music-player__song-img" />
              <div class="music-player__playlist-info  text_trsf-cap">
                <b class="text_overflow">{songName}</b>
                <div class="flex-row _justify_space-btwn">
                  <span class="music-player__subtitle">{artist}</span>
                  <span class="music-player__song-duration"></span>
                </div>
              </div>
            </div>
            <audio
              src={song}
              onLoadeddata={loadedAudio}
              onTimeupdate={updateTheProgressBar}
            />
          </li>
        );
      })}
    </ul>
  );
}

function Loading() {
  return (
    <div class="loading flex-row">
      <span class="loading__progress">0</span>
      <span>%</span>
    </div>
  );
}

function dom(tag, props, ...children) {
  if (typeof tag === "function") return tag(props, ...children);

  function addChild(parent, child) {
    if (Array.isArray(child)) {
      child.forEach((nestedChild) => addChild(parent, nestedChild));
    } else {
      parent.appendChild(
        child.nodeType ? child : document.createTextNode(child.toString())
      );
    }
  }

  const element = document.createElement(tag);

  Object.entries(props || {}).forEach(([name, value]) => {
    if (name.startsWith("on") && name.toLowerCase() in window) {
      element[name.toLowerCase()] = value;
    } else if (name === "style") {
      Object.entries(value).forEach(([styleProp, styleValue]) => {
        element.style[styleProp] = styleValue;
      });
    } else {
      element.setAttribute(name, value.toString());
    }
  });

  children.forEach((child) => {
    addChild(element, child);
  });

  return element;
}

fetch(
  "https://gist.githubusercontent.com/abxlfazl/37404417d17230a629683eb3f2f0a88a/raw/366ad64df645e94592847283a306fe2276de458e/music-info.json"
)
  .then((respone) => respone)
  .then((data) => data.json())
  .then((result) => {
    const songs = result.songs;

    function downloadTheFiles(media, input) {
      return Promise.all(
        input.map((song) => {
          const promise = new Promise((resolve) => {
            const url = song.files[media];
            const req = new XMLHttpRequest();
            req.open("GET", url, true);
            req.responseType = "blob";
            req.send();
            req.onreadystatechange = () => {
              if (req.readyState === 4) {
                if (req.status === 200) {
                  const blob = req.response;
                  const file = URL.createObjectURL(blob);
                  song.files[media] = file;
                  resolve(song);
                }
              }
            };
          });

          promise.then(() => {
            loadingProgress++;
            const progress = Math.round(
              (loadingProgress / (songs.length * 2)) * 100
            );
            loadingProgress_elmnt.innerHTML = progress;
          });

          return promise;
        })
      );
    }

    root.appendChild(<Loading />);
    loadingProgress_elmnt = querySelector(".loading__progress");

    downloadTheFiles("cover", songs).then((respone) => {
      downloadTheFiles("song", respone).then((data) => {
        root.removeChild(querySelector(".loading"));
        root.appendChild(<App songs={data} />);

        songsLength = data.length - 1;
        progress_elmnt = querySelector(".progress");
        playlistSongs_elmnt = querySelectorAll("audio");
        sliderImgs_elmnt = querySelector(".slider__imgs");
        songName_elmnt = querySelector(".music-player__subtitle");
        musicPlayerInfo_elmnt = querySelector(".music-player__info");
        singerName_elmnt = querySelector(".music-player__singer-name");
        selectedSong = playlistSongs_elmnt[indexSong];
        progressBar_elmnt = querySelector(".progress__bar");
        broadcastGuarantor_elmnt = querySelector(
          ".music-player__broadcast-guarantor"
        );

        controlSubtitleAnimation(musicPlayerInfo_elmnt, songName_elmnt);
        controlSubtitleAnimation(musicPlayerInfo_elmnt, singerName_elmnt);
      });
    });
  });

function controlSubtitleAnimation(parent, child) {
  if (child.classList.contains("animate")) return;

  const element = child.firstChild;

  if (child.clientWidth > parent.clientWidth) {
    child.appendChild(element.cloneNode(true));
    child.classList.add("animate");
  }

  setProperty(child.parentElement, "width", `${element.clientWidth}px`);
}
function handleResize() {
  const vH = window.innerHeight * 0.01;
  setProperty(document.documentElement, "--vH", `${vH}px`);
}
function querySelector(target) {
  return document.querySelector(target);
}
function querySelectorAll(target) {
  return document.querySelectorAll(target);
}
function setProperty(target, prop, value = "") {
  target.style.setProperty(prop, value);
}
function setBodyBg(color) {
  setProperty(document.body, "--body-bg", color);
}
function updateInfo(target, value) {
  while (target.firstChild) {
    target.removeChild(target.firstChild);
  }

  const targetChild_elmnt = document.createElement("div");
  targetChild_elmnt.appendChild(document.createTextNode(value));
  target.appendChild(targetChild_elmnt);
  target.classList.remove("animate");
  controlSubtitleAnimation(musicPlayerInfo_elmnt, target);
}
function handleScrub(e) {
  const progressOffsetLeft = progress_elmnt.getBoundingClientRect().left;
  const progressWidth = progress_elmnt.offsetWidth;
  const duration = selectedSong.duration;
  const currentTime = (e.clientX - progressOffsetLeft) / progressWidth;

  selectedSong.currentTime = currentTime * duration;
}

handleResize();

window.addEventListener("resize", handleResize);
window.addEventListener("orientationchange", handleResize);
window.addEventListener("transitionstart", ({ target }) => {
  if (target === sliderImgs_elmnt) {
    isLocked = true;
    setProperty(sliderImgs_elmnt, "will-change", "transform");
  }
});
window.addEventListener("transitionend", ({ target, propertyName }) => {
  if (target === sliderImgs_elmnt) {
    isLocked = false;
    setProperty(sliderImgs_elmnt, "will-change", "auto");
  }
  if (target.classList.contains("slider") && propertyName === "height") {
    controlSubtitleAnimation(musicPlayerInfo_elmnt, songName_elmnt);
    controlSubtitleAnimation(musicPlayerInfo_elmnt, singerName_elmnt);
  }
});
window.addEventListener("pointerup", () => {
  if (progressBarIsUpdating) {
    selectedSong.muted = false;
    progressBarIsUpdating = false;
  }
});
window.addEventListener("pointermove", (e) => {
  if (progressBarIsUpdating) {
    handleScrub(e, this);
    selectedSong.muted = true;
  }
});
View Compiled

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.