When I first played Hi-Fi Rush, the rhythm-based action gameplay lit up something in my mind—what if I could make a UI dance to the beat too? Inspired by the game, I set out to create a small interactive demo where shapes "dance" in sync with a beat, changing colors and animations as they pulse to music. Here's a breakdown of how I built it, so you can jump in and maybe try your own remix.
Step 1: Laying Out the HTML Structure
At the core of this experiment is a simple HTML structure of shapes and a song selector. Each shape in the UI represents a different beat frequency (quarter, half, double, etc.), creating a complex visual rhythm as the animations sync with the tempo.
<div class="parent">
<div class="dancer shape square common-beat"></div>
<div class="dancer shape line quarter-beat"></div>
<div class="dancer shape triangle third-beat"></div>
<!-- More shapes here -->
<p class='common-beat dancer'>
Beats per Minute: <span class='bpm'></span>
</p>
<form class='dancer common-beat'>
<!-- Song choices here -->
<button class='dancer common-beat rotate'>
Play
</button>
</form>
</div>
Each .dancer
class in the markup is a beat-timed element that we control through CSS animations and JavaScript. The bpm
field shows the current tempo, and a song choice form lets users pick different tracks to vibe with.
Step 2: CSS Animations to Match the Beat
Here, CSS handles the heavy lifting for animation timing, color shifts, and shape manipulation. I set up the CSS variables to represent different beat times based on the BPM (beats per minute), allowing us to scale the animations in real time.
html {
--bpm: 100; /* Initial BPM, updated in JS */
--common-time-beat: calc(60s / var(--bpm));
--quarter-time-beat: calc(var(--common-time-beat) * 4);
/* Additional beat calculations for timing variations */
}
@keyframes beat {
0% { scale: 0.8; }
14% { scale: 1.1; box-shadow: -5px -5px var(--color); }
96% { scale: 1; }
100% { scale: 0.8; }
}
@keyframes rotate {
0% { transform: rotate(-5deg); }
14% { transform: rotate(3deg); }
100% { transform: rotate(10deg); }
}
Each shape, like .square
or .hexagon
, has specific animations applied at different beat intervals. This creates the visual illusion of each shape "dancing" to a unique beat, adding variety and depth to the animation.
Step 3: Syncing Animations to Music with JavaScript
The JavaScript here connects everything together. I used the music-tempo
library to calculate the tempo of each audio track, which then drives the animations.
Setting up the BPM
Once a track is selected and played, we calculate its BPM and set the root variable --bpm
based on that tempo.
async function addAnimation() {
const bpm = (await calcTempo()).tempo;
$bpm.innerHTML = bpm;
$html.style.setProperty("--bpm", bpm);
$dancers.forEach(function (dancer) {
dancer.style.animation =
"var(--beat-time) var(--action, beat) infinite forwards";
});
}
This function updates the --bpm
variable, which in turn recalculates all timing-related CSS variables like --quarter-time-beat
, --double-time-beat
, etc., ensuring that each element’s animation frequency matches the song’s tempo.
Animating the Colors
The colors shift based on the beat duration, giving the UI a dynamic, pulsing feel that syncs with the track. Using setInterval
, I updated the hue to slowly evolve, which adds another layer of visual interest to the composition.
function shiftColors(beatDuration) {
setInterval(() => {
let hue = 36 + Number($html.style.getPropertyValue("--hue"));
$html.style.setProperty("--hue", hue);
}, beatDuration * 4 * 1000);
}
Step 4: Audio Analysis for Tempo Detection
Finally, using an AudioContext
, we fetch and decode audio data to get the tempo with music-tempo
. This was key for synchronizing animations without manually defining the tempo for each track.
async function getAudioBufferData(url) {
let context = new AudioContext();
const response = await fetch(url);
const arrayBuffer = await response.arrayBuffer();
const audioBuffer = await context.decodeAudioData(arrayBuffer);
return audioBuffer;
}
async function calcTempo() {
const buffer = await getAudioBufferData(trackUrl);
let audioData = buffer.getChannelData(0);
let mt = new musicTempo(audioData);
return mt;
}
The function calcTempo
performs the tempo analysis, providing the BPM necessary to drive the animations. Once we have the tempo, it’s off to the beat-driven races!
Final Thoughts
This project turned out to be a fun blend of CSS, JavaScript, and a bit of music analysis. Watching shapes dance in sync with the beat was incredibly satisfying and gave me a whole new appreciation for interactive UI elements.
check it out https://codepen.io/beejsbj/pen/BaeoNPG