Audio Visualizer
Web Audio API audio spectrum visualization drawn in canvas 2D, in bar, mirror, and radial styles, able to analyze a real audio source or drive a decorative animation from synthetic data.
This is a WebberUI Pro component
The install command below needs a licence to run; without one it returns 401.
Bring this into your project
Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.
npx webberui@latest add "https://webberui.com/r/audio-visualizer.en.json?t=<install token>"Audio Visualizer is a React audio visualizer (music spectrum and sound wave animation) built with the Web Audio API and canvas 2D, offering bars, a center-line mirror and a circular radial style. Connect an <audio>/<video> element or a microphone stream to read the live spectrum; without a source it falls back to a synthetic demo animation.
Playground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<AudioVisualizer/>
The complete example includes the preview layout and data. Install the component first, then paste this file into your project.
When to use
- Playback state for podcast players, music players or episode pages
- "Listening" feedback for voice input, voice messages or an AI voice assistant
- Microphone level checks in meeting or recording tools
- A decorative hero background for music products or event pages (no source, synthetic data)
- Not a good fit: a waveform users drag to seek; use Waveform Scrubber instead
Installation
npx webberui@latest add "https://webberui.com/r/audio-visualizer.en.json?t=<install token>"Or, once registries are configured in components.json, install it as @webberui/audio-visualizer.
Usage
With no audio source provided, the component is driven by procedurally synthesized data, which makes it a good fit for a decorative animation:
import { AudioVisualizer } from "@/components/ui/audio-visualizer";
<div className="h-40 overflow-hidden rounded-xl">
<AudioVisualizer variant="bars" />
</div>Analyzing a real media element
Pass the ref of an <audio> / <video> element to media and the component reads the live spectrum through the Web Audio API. The media's sound is played back through the audio graph, so it stays audible:
"use client";
import * as React from "react";
import { AudioVisualizer } from "@/components/ui/audio-visualizer";
export function Player() {
const audioRef = React.useRef<HTMLAudioElement>(null);
const [playing, setPlaying] = React.useState(false);
return (
<div>
<audio ref={audioRef} src="/track.mp3" />
<AudioVisualizer
variant="mirror"
media={audioRef}
active={playing}
color="#22d3ee"
className="h-40"
/>
<button
onClick={() => {
const el = audioRef.current;
if (!el) return;
if (playing) el.pause();
else void el.play();
setPlaying(!playing);
}}
>
Play / Pause
</button>
</div>
);
}Analyzing the microphone
Pass the MediaStream you get from getUserMedia to stream (the microphone is not played back, which avoids feedback):
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
<AudioVisualizer variant="radial" stream={stream} sensitivity={1.4} />Examples
Spectrum under a video player
media also accepts a <video> ref. Sync active with the video's play/pause events so the canvas freezes into a still silhouette while paused.
"use client";
import * as React from "react";
import { AudioVisualizer } from "@/components/ui/audio-visualizer";
export function EpisodePlayer() {
const videoRef = React.useRef<HTMLVideoElement>(null);
const [playing, setPlaying] = React.useState(false);
return (
<div className="space-y-3">
<video
ref={videoRef}
src="/episodes/ep-12.mp4"
controls
onPlay={() => setPlaying(true)}
onPause={() => setPlaying(false)}
/>
<AudioVisualizer
media={videoRef}
active={playing}
bars={32}
smoothing={0.6}
className="h-16"
label="Episode 12 audio spectrum"
/>
</div>
);
}Microphone capture and release
Call getUserMedia only after the user clicks a button. When stopping, stop the stream's tracks and set stream back to null.
const [stream, setStream] = React.useState<MediaStream | null>(null);
async function startListening() {
setStream(await navigator.mediaDevices.getUserMedia({ audio: true }));
}
function stopListening() {
stream?.getTracks().forEach((track) => track.stop());
setStream(null);
}
<AudioVisualizer
variant="mirror"
stream={stream}
active={stream !== null}
sensitivity={1.6}
color="#ef4444"
className="h-12"
label="Microphone level"
/>Decorative background without a source
With neither media nor stream, it runs on synthetic data and works as a background for a music event page.
<section className="relative h-80 overflow-hidden rounded-2xl bg-neutral-950">
<AudioVisualizer variant="radial" bars={96} color="#a78bfa" className="absolute inset-0 opacity-60" />
<h2 className="relative z-10 p-10 text-3xl font-semibold text-white">Year-end playlist is live</h2>
</section>Props
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "bars" | "mirror" | "radial" | "bars" | Visualization style: bars from the bottom, mirrored about a center line, or radial around a circle |
media | RefObject<HTMLMediaElement | null> | — | Ref of the <audio>/<video> to analyze; once provided, the real spectrum is analyzed |
stream | MediaStream | null | null | Supply an audio stream directly (the microphone, for example); takes precedence over media |
active | boolean | true | Whether the animation runs; at false it freezes into a still low-amplitude state |
bars | number | 48 | Number of frequency bands (rays for radial), clamped internally to 4–256 |
color | string | a theme-following neutral | Primary color; when not specified it follows currentColor (which switches with the light/dark theme) |
fftSize | number | 2048 | FFT window size; must be a power of two (32–32768) |
smoothing | number | 0.8 | Spectrum smoothing coefficient 0–1; the higher, the smoother and the slower to react |
sensitivity | number | 1 | Sensitivity multiplier, amplifying the overall amplitude |
label | string | "Audio Visualization" (Audio visualization) | Accessible label applied as the container's aria-label |
className | string | — | Class appended to the outermost container |
How it works
- Two data-source modes. When
mediaorstreamis provided, anAnalyserNodeis created and the real spectrum is read each frame withgetByteFrequencyData, then grouped intobarsbands on an "approximately logarithmic" scale that emphasizes the low end; with no audio source it falls back to procedurally synthesized data (multi-layer sine oscillation plus beat pulses), so a demo environment can show it off without any external audio file - A single canvas 2D layer. All three styles are drawn on the same
<canvas>:barsrise from the bottom,mirrormirrors above and below a center line, andradialradiates outward around a circle; each frame is redrawn after aclearRect, with rounded corners from the nativeroundRect(falling back to hand-drawnarcTowhere it is unsupported) - MediaElementSource can only be created once. A given media element can only have a source node created for it once in its whole lifetime, and creating one again throws; the component caches the "context / source / analyser" audio graph in a
WeakMapand reuses it across remounts and re-renders, releasing it automatically once the element is collected - AudioContext lifecycle. A context created from a microphone
streamisclose()d on unmount; the audio graph for a media element is not closed, because it is cached for reuse. If the context issuspended(the browser requires a user gesture),resume()is attempted whileactive - The real canvas size is set from
devicePixelRatio(capped at 2) and then scaled withsetTransform, so Retina screens stay sharp; aResizeObserverwatches for container changes and redraws immediately - When
coloris not specified, the canvas'scurrentColoris read (resolved from atext-neutral-*class), and aMutationObserverwatches class changes on<html>so the color is re-read the moment the light/dark theme switches
Accessibility
- The container carries
role="img"and a customizablearia-label(“Audio Visualization”, Audio visualization, by default), while the inner<canvas>is markedaria-hiddenso the per-frame image is not announced - When the user has "reduce motion" enabled at the system level, the
requestAnimationFrameloop is disabled entirely and only a single still low-amplitude silhouette is rendered, with the DOM structure unchanged (no two-stage mount needed) - The visualization is a decorative mapping of sound the user is already hearing; make sure the actual playback controls (the play/pause button and so on) have complete keyboard and labelling support of their own
FAQ
Does it work with the Next.js App Router?
Yes. The file already starts with "use client", so import it from a Server Component page as usual. Browser APIs such as AudioContext and ResizeObserver are only used inside effects.
Does it keep drawing after scrolling offscreen?
No. An IntersectionObserver watches the canvas and cancels the requestAnimationFrame loop when it leaves the viewport, resuming when it comes back. When active is false or reduced motion is on, the loop never starts.
How do I change the colour?
Pass color; any CSS colour value the canvas accepts works. Without it, the colour comes from the canvas's text-neutral-800/dark:text-neutral-200 classes and is re-read whenever the <html> class changes.
Will the music go silent when connected?
No. With media, the audio graph routes the sound back to the output so it stays audible. With stream (a microphone), it deliberately does not play back, to avoid feedback.
Why does it stay flat after connecting a source?
Browsers usually require a user gesture before an AudioContext can run. When the context is suspended, the component tries resume() while active is true, so set active to true after the user presses play.
Custom Video Player
A video player with a custom control bar that fades out automatically during playback, supporting scrub-to-seek, volume, fullscreen, and keyboard operation.
Image Compare
Drag the divider in the middle to compare a before and an after layer, clipped with clip-path and following smoothly on a spring.