Frontend Development 15 min read

How to Build a Mouse‑Tracking Parallax Banner Animation with HTML, CSS, and JavaScript

This tutorial explains how to recreate Bilibili's mouse‑responsive top‑banner animation by collecting assets, constructing a layered HTML structure, applying CSS positioning, and writing vanilla JavaScript to track mouse movement, calculate element offsets, and animate the layers with smooth transitions.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
How to Build a Mouse‑Tracking Parallax Banner Animation with HTML, CSS, and JavaScript

The article demonstrates how to implement the animated banner seen on Bilibili’s PC homepage, which reacts to mouse movement using a parallax effect.

Analysis : The effect is essentially a mouse‑driven parallax animation that moves layered elements based on the cursor’s relative position.

Collecting assets : Using the browser’s developer tools, the author identified a div with class animated‑banner containing multiple absolutely‑positioned div layers, each holding an img or video . All 23 images and one video were downloaded locally.

HTML structure (copying the original markup):

...

CSS (basic layout and layer styling):

body * {
  margin: 0;
  padding: 0;
}

.animated-banner {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  overflow: hidden;
  min-width: 1000px;
  min-height: 155px;
  height: 9.375vw;
}

.animated-banner > .layer {
  position: absolute;
  left: 0;
  top: 0;
  height: 100%;
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
}

Video handling : Play the video programmatically to avoid autoplay restrictions; add the muted attribute for browsers that block autoplay without user interaction.

(function(){
  const video = document.querySelector('video');
  video.play();
})();

Mouse listeners : Add mouseenter and mouseleave events to start and stop tracking.

const banner = document.querySelector('.animated-banner');
banner.addEventListener('mouseenter', function(event){
  event.stopPropagation();
  startListener();
});
banner.addEventListener('mouseleave', function(event){
  event.stopPropagation();
  clearListener();
});

Tracking mouse movement : On mousemove , obtain the cursor’s pageX, compute the offset relative to the banner, and log the event.

function startListener(){
  banner.addEventListener('mousemove', function(event){
    event.stopPropagation();
    console.log(event);
  });
}

Calculating offsets : Record the initial mouse X position ( initMouseLeft ) on mouseenter , then compute a proportional offset for each layer.

const bannerWidth = banner.offsetWidth;
let initMouseLeft = 0;
function calcutedPosition(mouseLeft){
  return -(mouseLeft - initMouseLeft) * 10 / bannerWidth; // example scale
}

Applying the offset to a layer (example for the second layer) :

function startListener(){
  banner.addEventListener('mousemove', function(event){
    const mouseLeft = event.pageX - bannerLeft;
    const cloudY = calcutedPosition(mouseLeft);
    const secondElement = banner.querySelector('.layer:nth-child(2) img');
    secondElement.style = `height: 187px; width: 2000px; transform: translate(0px, ${cloudY}px) rotate(0deg) scale(1); opacity: 1;`;
  });
}

Restoring the original position when the mouse leaves, using a simple linear interpolation over 200 ms.

function clearListener(){
  const secondElement = banner.querySelector('.layer:nth-child(2) img');
  const mouseLeft = event.pageX - bannerLeft;
  const cloudY = calcutedPosition(mouseLeft);
  let startValue = cloudY;
  let endValue = 0;
  const duration = 200;
  const interval = 50;
  const steps = duration / interval;
  const stepValue = (startValue - endValue) / steps;
  let currentValue = startValue;
  const timer = setInterval(() => {
    currentValue -= stepValue;
    secondElement.style = `height: 187px; width: 2000px; transform: translate(0px, ${currentValue}px) rotate(0deg) scale(1); opacity: 1;`;
    if (Math.abs(currentValue - endValue) < Math.abs(stepValue)) {
      clearInterval(timer);
      secondElement.style = `height: 187px; width: 2000px; transform: translate(0px, ${endValue}px) rotate(0deg) scale(1); opacity: 1;`;
    }
  }, interval);
}

Encapsulating styles : Create a styleMap that stores each element’s initial style, movement direction, and maximum offset, then iterate over the map on every mousemove to update the transform accordingly.

const styleMap = {
  0: {
    initialStyle: {height: '187px', width: '2000px', translateX: 0, translateY: 0, rotate: 0, scale: 1, opacity: 1},
    style: {direction: 'x', scale: 400},
    element: banner.querySelector('.layer:nth-child(1) img')
  },
  // ... other layers
};

const init = () => {
  Object.keys(styleMap).forEach(item => {
    const current = styleMap[item];
    const initStyle = current.initialStyle;
    current.element.style = `height: ${initStyle.height}; width: ${initStyle.width}; transform: translate(${initStyle.translateX}px, ${initStyle.translateY}px) rotate(${initStyle.rotate}deg) scale(${initStyle.scale}); opacity: ${initStyle.opacity};`;
  });
};

During mousemove , the script calculates each element’s offset based on its configured scale and direction, builds a new style string, and assigns it to the element.

banner.addEventListener('mousemove', function(event){
  const mouseLeft = event.pageX - bannerLeft;
  Object.keys(styleMap).forEach(item => {
    const current = styleMap[item];
    if (current.style) {
      const initStyle = current.initialStyle;
      const offset = calcutedPosition(mouseLeft, current.style.scale);
      let styleResult = `height: ${initStyle.height}; width: ${initStyle.width}; opacity: ${initStyle.opacity};`;
      if (current.style.direction === 'y') {
        styleResult += `transform: translate(${initStyle.translateX}px, ${initStyle.translateY - offset}px) rotate(${initStyle.rotate}deg) scale(${initStyle.scale});`;
      } else {
        styleResult += `transform: translate(${initStyle.translateX - offset}px, ${initStyle.translateY}px) rotate(${initStyle.rotate}deg) scale(${initStyle.scale});`;
      }
      current.element.style = styleResult;
    }
  });
});

Global variables used throughout the script:

const banner = document.querySelector('.animated-banner');
const bannerLeft = banner.offsetLeft;
const bannerWidth = banner.offsetWidth;
let initMouseLeft = 0;

Finally, the initialization functions are called on page load:

window.onload = function(){
  init();
  playVideo();
  touchListener();
};

The complete source files are available in the banner-mouse-animation repository, and the resulting animation closely matches the original Bilibili banner.

frontendJavaScriptCSShtmlmouse-trackingparallax
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.