Recreating Bilibili Home Page Banner with Native JavaScript and Major Frontend Frameworks
This article explains how to analyze Bilibili's homepage banner, extract its image layers and transformation data, and then implement the same effect using pure JavaScript as well as Angular, React, and Vue, providing complete source code and a step‑by‑step guide.
The author starts by describing how they first implemented Bilibili's banner using Angular and later decided to provide a version that works with plain JavaScript, React, and Vue so that anyone can copy the code directly into their projects.
By inspecting the DOM of Bilibili's banner, each image layer is found inside a div with class layer , and its position, size, rotation, scale and opacity are controlled via CSS transform and opacity properties.
The core idea is to treat one image as a baseline, record its movement values (translateX, translateY, rotate, scale, opacity) and then calculate the corresponding values for all other images based on that baseline.
First, the static resources are collected (image files, dimensions, initial transform values) and stored in a JavaScript array. The following snippet shows the data structure used:
{
type: 'image',
file: '[email protected]',
width: 1728,
height: 162,
x: 0,
y: 0,
r: 0,
s: 1,
o: 1,
newX: -1.17573,
newY: 0,
newRotate: 0,
bench: -1.17573
}Next, a simple static HTML page is built to display the layers using the collected data. The HTML skeleton looks like this:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.bili-banner { position: relative; width: 100%; height: 100%; background-color: #f9f9f9; display: flex; justify-content: center; }
.animated-banner { position: absolute; width: 100%; height: 100%; top: 0; left: 0; overflow: hidden; }
.layer { position: absolute; width: 100%; height: 100%; left: 0; top: 0; display: flex; align-items: center; justify-content: center; }
</style>
</head>
<body>
<div class="banner-container" id="winter-5"></div>
<script>/* JavaScript code will be inserted here */</script>
</body>
</html>The JavaScript creates the DOM elements for each layer, sets the image source, size and initial transform, and appends them to the container:
const baseSrc = './assets/winter-5/';
const imgData = [...]; // the array shown above
const container = document.getElementById('winter-5');
const biliBanner = document.createElement('div');
biliBanner.className = 'bili-banner';
const animatedBanner = document.createElement('div');
animatedBanner.className = 'animated-banner';
imgData.forEach(item => {
const layer = document.createElement('div');
layer.className = 'layer';
const img = document.createElement('img');
img.src = baseSrc + item.file;
img.style.width = item.width + 'px';
img.style.height = item.height + 'px';
img.style.transform = `translate(${item.x}px, ${item.y}px) rotate(${item.r}deg) scale(${item.s})`;
img.style.opacity = item.o;
layer.appendChild(img);
animatedBanner.appendChild(layer);
});
biliBanner.appendChild(animatedBanner);
container.appendChild(biliBanner);To achieve the interactive effect, a class BilibiliBannerBase is defined. It registers mouse events ( mouseenter , mousemove , mouseleave ), tracks the mouse movement, computes the delta, and updates each image's transform based on the previously recorded baseline values. The class also uses ResizeObserver to adapt to container size changes and runs a requestAnimationFrame loop to render at a stable frame rate.
class BilibiliBannerBase {
constructor({container, imgData, marginLeft = 0, moveRate = 300, maxMove, maxLeftPosition, maxRightPosition}) {
this.container = container;
this.imgData = imgData;
this.marginLeft = marginLeft;
this.moveRate = moveRate;
// ...initialisation of observers, event listeners, and rendering loop
}
// mouse event handlers
bilibiliStart(x, y) { this.startPoint = {x, y}; this.transition = 0; }
bilibiliMove(x, y) { /* calculate moveX, update each layer */ }
bilibiliEnd() { this.startPoint = {x:0, y:0}; }
// rendering logic
render() { this.imgDomList.forEach((img,i)=>{ /* apply new transform and opacity */ }); }
loop(timestamp) { /* requestAnimationFrame loop */ }
// other helper methods (getLeftWidth, getRightWidth, reset, destroy)
}
let banner = new BilibiliBannerBase({
container: document.getElementById('winter-5'),
imgData: imgData,
maxMove: {left: 2000, right: 2000}
});The final result closely matches the original Bilibili banner, with all images moving smoothly according to mouse movement. The full source code for the native JavaScript version, as well as the Angular, React, and Vue implementations, are available on Gitee (https://gitee.com/CrimsonHu/bilibili-banner).
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.