Originally published on Substack (opens in a new tab)

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

Back to all writing