DEV Community

Bryan Hamilton
Bryan Hamilton

Posted on

Building a Real Audio Hover Preview with Zero Dependencies

I run The DJ Calendar, a real-time EDM tour date platform. The homepage has 99 artist cards. I wanted you to be able to hover any of them and hear their music instantly — without navigating away, without a page load, without a framework.

Here's how I built it.

The Stack

Zero. No React, no Vue, no jQuery, no audio player library. Just vanilla JavaScript and two browser APIs that ship with every modern browser:

  • iTunes Search API — free 30-second audio previews
  • Web Audio API — real frequency analysis for the equalizer

Step 1: Getting the Audio

Apple's iTunes Search API is a public endpoint that doesn't need authentication. A single request with entity=musicTrack&limit=12&media=music returns preview URLs for any artist.

fetch(`https://itunes.apple.com/search?term=${encodeURIComponent(name)}&entity=musicTrack&limit=12&media=music`)
  .then(r => r.json())
  .then(data => {
    const result = data.results.find(r => r.previewUrl);
    if (result) {
      card.dataset.previewUrl = result.previewUrl;
      card.dataset.previewTitle = result.trackName;
    } else {
      card.dataset.previewMiss = '';
    }
  });
Enter fullscreen mode Exit fullscreen mode

The preview URL gets cached on the card's data- attribute. First hover does the lookup. Every hover after that is instant.

Step 2: The Analyser — Real Frequencies, Not a Fake

This is the part I'm proudest of. The 8-bar equalizer isn't animated CSS pretending to be sound. It reads actual frequency data from the audio.

const audio = new Audio();
audio.crossOrigin = 'anonymous';

const ctx = new AudioContext();
const src = ctx.createMediaElementSource(audio);
const analyser = ctx.createAnalyser();
analyser.fftSize = 64;
analyser.smoothingTimeConstant = 0.8;

src.connect(analyser);
analyser.connect(ctx.destination);

const bufferLength = analyser.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength);

function draw() {
  analyser.getByteFrequencyData(dataArray);
  // Clear canvas, draw 8 bars from dataArray[i*2]
  requestAnimationFrame(draw);
}
Enter fullscreen mode Exit fullscreen mode

The fftSize of 64 gives us 32 frequency bins. Reading every other one (dataArray[i*2]) gives us exactly 8 bars. Each bar represents a real slice of the audio spectrum.

Step 3: The Race Condition Guard

Sweep your cursor across a row of cards fast and three things race: the audio load, the canvas draw loop, and your mouse leaving. Here's the fix:

card._djcPreviewGen = (card._djcPreviewGen || 0) + 1;
const gen = card._djcPreviewGen;

// Inside every async callback:
if (gen !== card._djcPreviewGen) return; // stale, abort
Enter fullscreen mode Exit fullscreen mode

New hover bumps the counter. Every previous hover's callbacks check before touching the DOM. The orphaned callbacks just die silently.

Step 4: Consent-First Design

Most hover previews break on the devices where hover doesn't exist. This one treats every "no" as a first-class answer.

Touch devices (hover: none, pointer: coarse): hover never fires. A tap on the photo toggles play/stop. The tap also satisfies the autoplay policy — no extra click needed.

Reduced motion (prefers-reduced-motion: reduce): no hover preview at all. Motion-reactive feedback is exactly what these visitors asked the OS to skip.

No preview found: the card gets data-preview-miss and is silently skipped forever. No repeated failing lookups on every hover.

@media (hover: none) and (pointer: coarse) {
  .dj-card .card-image-overlay { display: none; }
}
@media (prefers-reduced-motion: reduce) {
  .dj-card .card-image-overlay { display: none; }
}
Enter fullscreen mode Exit fullscreen mode

Step 5: The Extras

A few things that came for free once the audio pipeline was in place:

  • Document title swap: while a preview plays, the tab reads "David Guetta - Titanium (feat. Sia)". Restores on stop.
  • Media Session API: the track shows up in OS media controls with artwork.
  • Cross-origin audio: audio.crossOrigin = 'anonymous' is what makes the analyser legal on Apple's CDN. Without it, the analyser returns all zeros.

The Result

A hover preview that works on every device, respects every accessibility setting, handles race conditions, and drives a real audio-reactive equalizer — all in vanilla JS, zero dependencies.
[Try it Live] https://bryanhamiltondev.github.io/dj-card-preview/ — click the page once, then hover the card.

The full source is on GitHub: bryanhamiltondev/dj-card-preview


This is part of a series extracting production patterns from The DJ Calendar. Also check out the eq-visualizer (pure-CSS decorative EQ) and next-show-radar (consent-first tour map).

Top comments (0)