Bryan Hamilton / projects / dj-card-preview

bryanhamilton.info / open source / dj-card-preview

dj-card-preview Hover an artist. Hear their music.

The hover preview from The DJ Calendar, extracted as a standalone widget. Roll over an artist and a 30-second iTunes preview plays while an 8-bar EQ reacts to the audio's actual frequencies. Roll off, and everything stops.

  • Zero dependencies
  • Web Audio API
  • Runs on 99 production cards
  • MIT license

Anyma

Looking up a preview…

Sound on. Click anywhere once, then hover the card.

In brief: dj-card-preview is a zero-dependency JavaScript widget that plays a 30-second music preview from an artist card and visualizes its real audio frequencies with an eight-bar Web Audio equalizer. Touch, reduced-motion, and conflicting-audio states are handled explicitly.

Why it exists

The DJ Calendar's homepage is a grid of 99 artist cards. A photo tells you who a DJ is; it doesn't tell you what they sound like. Hovering a card and hearing thirty seconds of their music answers that instantly, without leaving the page, opening a player or signing in to anything.

The hard part isn't playing audio on hover. It's doing it in a way that never misbehaves: on touch screens where hover doesn't exist, for visitors who've asked their device for less motion, when another player is already running, and when someone sweeps their cursor across ten cards in a second.

Real frequencies, not a fake animation

Plenty of "audio visualizers" are a looping CSS animation that would bounce just the same in silence. This one is wired to the sound. The preview plays through an <audio> element, createMediaElementSource routes it into an AnalyserNode, and on every animation frame each of the eight bars reads its own frequency bin.

const source   = ctx.createMediaElementSource(audio);
const analyser = ctx.createAnalyser();
analyser.fftSize = 64;                  // 32 frequency bins: plenty for 8 bars
analyser.smoothingTimeConstant = 0.8;   // calm, readable movement
source.connect(analyser).connect(ctx.destination);

const bins = new Uint8Array(analyser.frequencyBinCount);
(function draw() {
  analyser.getByteFrequencyData(bins);    // the real spectrum, this frame
  drawEightBars(bins);
  raf = requestAnimationFrame(draw);
})();

Simplified sketch of the technique. The production code is in src/ on GitHub.

One detail makes this legal on Apple's preview files: the audio element is created with crossOrigin = "anonymous". Without it, the browser treats cross-origin audio as opaque and the analyser reads nothing but zeros.

Every "no" is designed

Most hover-preview widgets fail loudly on the devices and settings they weren't built for. This one treats each "no" as a first-class answer:

Touch devices
On (hover: none) and (pointer: coarse), hover never fires. A tap on the photo toggles play and stop, and that tap is also the user gesture browsers require for sound.
Reduced motion
With prefers-reduced-motion: reduce, there's no hover preview at all. A motion-reactive EQ is exactly what these visitors asked their device to skip.
Another player is running
When the site's favorites player is active, card previews stand down, then hand control back when they're done.
No preview found
The card is marked data-preview-miss and quietly skipped from then on: no repeated failing lookups on every hover.

The race-condition guard

Sweep your cursor across a row of cards quickly and three things race each other: the audio loading, the canvas draw loop, and your mouse leaving. Without a guard, a preview that finished loading after you moved on would start playing on a card you're no longer pointing at.

The fix is a generation counter on each card. Every hover bumps it, and every async callback checks it before touching the audio or the DOM. A callback from an older generation is stale, so it does nothing. Try it on the demo: sweep in and out quickly and watch the counters under the card.

function onEnter(card) {
  const gen = ++card._previewGen;        // this hover's ticket
  loadPreview(card).then(url => {
    if (gen !== card._previewGen) return;  // stale: the cursor already left
    play(card, url);
  });
}
function onLeave(card) {
  card._previewGen++;                     // orphan everything the last hover started
  stop(card);
}

Simplified sketch. In production the counter is _djcPreviewGen, and it guards the audio load, the draw loop and the overlay together.

Extras that come free

Use it in your project

The widget binds itself to a grid and expects each card to follow this contract. The two data-preview-* attributes are an optional cache; leave them out and the first hover fills them in.

<a class="dj-card" href="/artist-slug/"
   data-id="davidguetta"
   data-name="David Guetta"
   data-image="https://.../artist.jpg"
   data-preview-url="https://audio-ssl.itunes.apple.com/...m4a"  <!-- optional cache -->
   data-preview-title="Titanium (feat. Sia)">                     <!-- optional cache -->
  <span class="card-image">
    <img class="card-image-photo" src="..." alt="...">
    <span class="card-image-overlay">
      <span class="card-overlay-label">You are now listening to:</span>
      <span class="card-overlay-track"></span>
    </span>
    <canvas class="waveform-canvas" width="280" height="40"></canvas>
  </span>
  <span class="dj-card-name">David Guetta</span>
</a>

From the repo's README. No build step, no framework, no dependencies: any browser from the last decade with Web Audio support.

Frequently asked questions

What does dj-card-preview do?

dj-card-preview adds a 30-second iTunes preview to artist cards and draws an eight-bar canvas equalizer from the audio’s real Web Audio frequencies.

How does the equalizer respond to music?

The audio element feeds a Web Audio AnalyserNode, and each canvas bar reads its own frequency bin while the preview plays.

Does the preview work on touch devices?

On touch devices, a tap toggles playback instead of relying on hover. The tap is also the user gesture needed to allow audio.

What happens when motion is reduced or no preview is available?

The widget disables hover preview for reduced-motion preferences and remembers a missing preview so it does not repeat the failed lookup on every hover.

Need someone who sweats these details?

Bryan Hamilton is a full-stack web engineer with 25+ years in PHP and JavaScript, available immediately for remote, hybrid or on-site roles in the NYC metro area.