Audio visualizer for a website, with the Web Audio API
Bars, rings or whole landscapes that move with the music. How a web audio visualizer turns sound into numbers you can draw, a canvas starter that plays a small synth loop and draws its spectrum, and the skills that build a finished music visualizer hero.
Free skills to start. Pro is $79 a year for every skill, or $9.99 a month for 10. See pricing
What is a audio visualizer?
An audio visualizer, or music visualizer, draws sound while it plays: a row of equalizer bars, a waveform line, a ring of spikes, ground that rises with the bass. On a website it suits podcasts, record labels, radio, artists, festivals and audio products, where the first screen can show that there is something to hear.
There are two kinds, and it matters which one you build. A real visualizer analyses the audio the page is playing, so a quiet intro barely moves it and a drop fills the screen. An animated one follows a beat written into its code: it looks musical and needs no audio file, but it hears nothing. Both have their place; only the first should be called audio-reactive.
Browsers add one rule that shapes the design: a page cannot start sound on its own. A visualizer needs a play button, and a look of its own for the time before anyone presses it.
How the effect works
The Web Audio API does the hard part. Route the sound through an AnalyserNode, and each frame getByteFrequencyData fills an array with the loudness of every frequency bin, from low to high. With an fftSize of 2048 you get 1,024 bins, each about 23 Hz wide at a 48 kHz sample rate. For a waveform instead of bars, getByteTimeDomainData gives you the wave itself.
The bins are evenly spaced in hertz, but hearing is not: the lowest octave fits in a bin or two, the top octave spreads over hundreds. Group the bins into bands spaced on a log scale, so bass, mids and treble each get a fair share of the bars. Then smooth each band over time, rising fast and falling slowly, so the bars hit with the beat without flickering.
01
Start on a click
Create the AudioContext, or resume it, inside a click handler. Browsers keep audio suspended until the visitor has interacted with the page.
02
Connect an analyser
Source, then AnalyserNode, then the speakers. The source can be an <audio> element through createMediaElementSource, the microphone through getUserMedia, or oscillators you play yourself.
03
Group bins into bands
Pick 32 to 64 bands spaced logarithmically from about 40 Hz to 16 kHz, and average the bins inside each one.
04
Smooth, then draw
Ease each band toward its new value, quickly on the way up and slowly on the way down, timed by the real time between frames. Then draw bars, a line or a ring.
HTML
An audio visualizer in plain JavaScript
A Play button starts a small synth loop made of oscillators, so there is no file to load, and 48 log-spaced bars draw its spectrum through an AnalyserNode. Until then the bars breathe gently. For your own track, connect an <audio> element with createMediaElementSource instead. Reduced motion gets a still frame; the sound still plays.
<div style="position:fixed;inset:0;background:#0e0b16">
<canvas id="viz" style="position:absolute;inset:0;width:100%;height:100%"></canvas>
<button id="play" aria-pressed="false" style="position:absolute;left:24px;bottom:24px;padding:10px 18px;border:0;border-radius:99px;background:#ff9e5e;color:#0e0b16;font:600 15px system-ui">Play demo</button>
</div>
<script>
const canvas = document.getElementById('viz')
const button = document.getElementById('play')
const g = canvas.getContext('2d')
const BANDS = 48, LOW = 40, HIGH = 16000 // log-spaced bands, in Hz
const still = matchMedia('(prefers-reduced-motion: reduce)').matches
const level = new Float32Array(BANDS)
let ctx, analyser, bins, noise, timer, playing = false, next = 0, step = 0
function size() {
const dpr = Math.min(devicePixelRatio, 2)
canvas.width = canvas.clientWidth * dpr
canvas.height = canvas.clientHeight * dpr
}
size(); addEventListener('resize', () => { size(); if (still) draw(0) })
// A small synth loop, so there is no file to load. For a real track:
// ctx.createMediaElementSource(audioElement).connect(analyser)
function note(type, freq, t, len, vol, drop) {
const o = ctx.createOscillator(), v = ctx.createGain()
o.type = type; o.frequency.setValueAtTime(freq, t)
if (drop) o.frequency.exponentialRampToValueAtTime(drop, t + len)
v.gain.setValueAtTime(vol, t); v.gain.exponentialRampToValueAtTime(0.001, t + len)
o.connect(v).connect(analyser); o.start(t); o.stop(t + len)
}
function hat(t) {
const n = ctx.createBufferSource(), f = ctx.createBiquadFilter(), v = ctx.createGain()
n.buffer = noise
f.type = 'highpass'; f.frequency.value = 7000
v.gain.setValueAtTime(0.25, t); v.gain.exponentialRampToValueAtTime(0.001, t + 0.05)
n.connect(f).connect(v).connect(analyser); n.start(t)
}
const ROOTS = [55, 55, 65.4, 49] // A, A, C, G
function schedule() { // queue sixteenth notes a little ahead of the clock
while (next < ctx.currentTime + 0.12) {
const root = ROOTS[Math.floor(step / 16) % 4], s = step % 16
if (s % 4 === 0) note('sine', 150, next, 0.3, 0.7, 40)
if (s % 2 === 1) hat(next)
if (s % 4 !== 0) note('sawtooth', root, next, 0.12, 0.18)
note('triangle', root * [4, 6, 8, 5][s % 4], next, 0.1, 0.08)
next += 0.125; step++
}
}
button.addEventListener('click', async () => {
if (!ctx) { // audio may only start after a click
ctx = new AudioContext()
analyser = ctx.createAnalyser()
analyser.fftSize = 2048; analyser.smoothingTimeConstant = 0
analyser.connect(ctx.destination)
bins = new Uint8Array(analyser.frequencyBinCount)
noise = ctx.createBuffer(1, ctx.sampleRate * 0.05, ctx.sampleRate)
noise.getChannelData(0).forEach((_, i, d) => (d[i] = Math.random() * 2 - 1))
next = ctx.currentTime + 0.05
}
playing = !playing
if (playing) { await ctx.resume(); timer = setInterval(schedule, 25); schedule() }
else { clearInterval(timer); await ctx.suspend() }
button.textContent = playing ? 'Pause' : 'Play demo'
button.setAttribute('aria-pressed', playing)
})
function target(i, t) {
if (!playing || still) return 0.1 + 0.06 * Math.sin(t * 1.5 + i * 0.35) // idle: breathe
const hz = (b) => LOW * Math.pow(HIGH / LOW, b / BANDS)
const per = ctx.sampleRate / analyser.fftSize
const a = Math.floor(hz(i) / per), z = Math.max(a + 1, Math.ceil(hz(i + 1) / per))
let sum = 0
for (let k = a; k < z; k++) sum += bins[k]
return Math.pow(sum / (z - a) / 255, 1.6) // a curve that keeps quiet bands low
}
let last = performance.now()
function draw(now) {
const dt = Math.min(0.1, Math.max(0, now - last) / 1000); last = now
if (playing && !still) analyser.getByteFrequencyData(bins)
const w = canvas.width, h = canvas.height, gap = w / BANDS
g.fillStyle = '#0e0b16'; g.fillRect(0, 0, w, h)
for (let i = 0; i < BANDS; i++) {
const v = target(i, now / 1000)
const speed = v > level[i] ? 0.02 : 0.18 // rise fast, fall slowly
level[i] = still ? v : level[i] + (v - level[i]) * (1 - Math.exp(-dt / speed))
const bar = Math.max(2, level[i] * h * 0.7)
g.fillStyle = 'hsl(' + (24 + level[i] * 20) + ',100%,' + (55 + level[i] * 25) + '%)'
g.fillRect(i * gap + gap * 0.15, h * 0.85 - bar, gap * 0.7, bar)
}
if (!still) requestAnimationFrame(draw)
}
requestAnimationFrame(draw)
</script>A starting point written for this page, free to use in your own projects. The skills below build the finished version with checks, fallbacks and a recorded video.
What to get right on a real page
01
Never autoplay
Start sound only from a button the visitor presses, show that it is playing, and keep pause in the same place. Say in the copy that there is something to hear.
02
Design the idle state
Most visitors never press play. Give the visualizer a quiet look of its own, a slow breathing or a still frame, so the hero is finished before any sound starts.
03
Serve the audio from your site
Audio from another domain without CORS headers (Access-Control-Allow-Origin) comes through the Web Audio graph as silence, so the bars stay flat. Host the file next to the page, and test it served over http, not opened from a file.
04
Ask before using the microphone
A microphone visualizer needs getUserMedia, which works only on https pages and localhost and shows a permission prompt. Offer it as a toggle the visitor turns on, and connect the stream to the analyser only, never to the speakers.
05
Hold a still frame for reduced motion
Respect prefers-reduced-motion: show one finished frame instead of the loop. A visitor who turned motion off still gets the look.
Skills that build a audio visualizer
Finished music visualizer heroes with your colors, fonts, logo and words, checked at four widths, with a 1080p video for launch posts. The first listens to real audio; the second moves to a beat written into its code.
- Audio Visualizer HeroClaude CodeCodexCursor+3 more agents
Motion Heroes · Pro
Audio Visualizer Hero
Audio-reactive hero section · page + embed + 1080p video with sound
A hero that really listens. Your track, started by a play button and never autoplayed, or the visitor’s microphone, is analysed live with the Web Audio API, one AnalyserNode per stereo channel, and drawn as a valley at dusk: ridgelines of the spectrum, treble meeting in the middle and bass rising into mountains at the sides. A dozen times a second the current ridge is stamped into the land and slides back toward a low sun that blooms with the bass and flares on kicks, and the video carries the track’s own sound.
Worth knowing: It plays one theme or demo track, not a library of episodes, and the Mic toggle shows only on https pages and localhost.
Needs: Node.js 18+, Chrome (or Chromium/Edge), ffmpeg, and an audio file (or the included loop generator)
See what Audio Visualizer Hero makes - Circular Equalizer HeroClaude CodeCodexCursor+3 more agents
Motion Heroes · Pro
Circular Equalizer Hero
Animated hero section · page + embed + 1080p video
An animated circular visualizer: radial equalizer bars round a turning vinyl record, bass at the bottom and treble at the top. A generated beat drives it, so the kick throws the bass outward and makes the ring breathe, the snare lifts the middle, hats flicker at the top and a four-chord pad moves slow peaks, with a waveform loop round the record and sparks on each kick. You set the tempo, from 70 to 170 BPM, and the drum pattern; no audio file is needed.
Worth knowing: It does not react to real audio, and the page and video are silent. You can lay your own track under the video at the same tempo.
Needs: Node.js 18+, Chrome (or Chromium/Edge), and ffmpeg for the video
See what Circular Equalizer Hero makes
Install
Build a audio visualizer with your agent
Each skill is a SKILL.md folder, so it installs the same way in Claude Code, Codex, Cursor and other agents. The example below uses Audio Visualizer Hero; each skill page has its own prompt.
1. Paste the install prompt
Your agent runs the installer. The first time, it shows a sign-in link so you can approve it in the browser.
Install the "Audio Visualizer Hero" skill from Lookbk (https://lookbk.dev/skills/audio-visualizer-hero). 1. Run: curl -fsSL https://lookbk.dev/cli.js | node - add audio-visualizer-hero 2. If it prints a sign-in link, show it to me. When I say I approved it, run the same command again. 3. When the install finishes, read the SKILL.md it installed and tell me what you need from me to make the first result.
Or run it in a terminal
curl -fsSL https://lookbk.dev/cli.js | node - add audio-visualizer-hero2. Start a new session
Agents load skills when a session starts. The installer writes the folder where your agent looks:
- Claude Code
- ~/.claude/skills/audio-visualizer-hero
- Codex
- ~/.agents/skills/audio-visualizer-hero
- Cursor
- ~/.agents/skills/audio-visualizer-hero
3. Ask for the result
Describe it in plain words, for example:
Build an Audio Visualizer hero for Halfnight, our podcast studio: night sky, amber sun, cream ridgelines, our station theme in the player, headline 'Shows people stay up for.', and record a 12-second video with the theme under it.
Another agent? Install a skill in any agent
FAQ
Questions
Still stuck? The guides cover installing, agents and licensing in more detail.
Read the guidesHow do I make an audio visualizer in JavaScript?
Create an AudioContext in a click handler, route an <audio> element through an AnalyserNode with createMediaElementSource, and read getByteFrequencyData on every animation frame. Group the bins into log-spaced bands and draw a bar for each. The starter does the same with a synth loop in place of a file.
Can I make an audio visualizer with CSS only?
Not a real one. CSS cannot hear audio, so it can only animate bars to a fixed rhythm. To react to sound, read the levels with the Web Audio API and draw them on a canvas, or pass them to CSS custom properties.
Why does my visualizer stay flat?
Usually the AudioContext is still suspended because it was not created or resumed in a click, or the audio comes from another domain without CORS headers, so the analyser receives silence. Check the context’s state, and serve the file from your own site.
Can a website visualizer use the microphone?
Yes, on an https page and after the visitor allows it. Connect the stream from getUserMedia to the analyser with createMediaStreamSource, and not to the speakers, or it will feed back. Audio Visualizer Hero offers a Mic toggle whose stream only feeds its analysers: nothing is recorded or sent.
What is the difference between the two skills?
Audio Visualizer Hero analyses real sound, your track or the microphone, and records its video with that track. Circular Equalizer Hero animates to a beat generated in code: it needs no audio file and every frame is the same on every machine, but its page and video are silent.
Can I get the visualizer as a video with sound?
Yes. Audio Visualizer Hero records a 1080p MP4 with the track as AAC, frame-exact and seamless for looping tracks, with every frame drawn from the same spectrum the browser computes. It can also write an original music loop if you have no track.

