DEV Community

Cover image for Audio That Sees Itself
Giovambattista Fazioli
Giovambattista Fazioli

Posted on

Audio That Sees Itself

A Mantine-native audio player for React with waveform visualisation and a live spectrum analyser, built on the Web Audio API. Same three-layer architecture as Mantine Video — drop-in <Audio />, composable compound API, headless useAudio hook — but designed for a medium that has no frame to look at.

Introduction

Mantine Video shipped last week. The next obvious step was to give audio the same treatment — a polished default player, a composable control bar, a headless hook for fully custom UIs. The catch is that audio has no frame to look at: a 30-second video communicates its content the instant you hit play, while a 30-second audio file looks like a thin grey line. So the new component does the opposite of what most React audio players do — it makes the audio look at itself: decoded waveform on <canvas>, live FFT spectrum on the analyser node, every part theme-aware.

Mantine Audio Waveform

What is mantine-audio?

@gfazioli/mantine-audio wraps the native HTML <audio> element with three layers, layered from "drop-in" to "fully custom":

  • A polished default <Audio /> factory with four built-in variants (overlay, minimal, floating, bordered) and five sizes (xsxl), all CSS-driven via [data-variant] / [data-size] attributes.
  • A composable compound API — ten sub-components you can reorder, replace or restyle: Audio.Controls, Audio.PlayButton, Audio.SkipButton, Audio.Timeline, Audio.TimeDisplay, Audio.MuteButton, Audio.VolumeSlider, Audio.SpeedControl, Audio.Waveform, Audio.Spectrum.
  • A fully headless useAudio hook with 16 state values, 12 actions, the decoded peaks, the AnalyserNode, and currentSrc — to drive a 100% custom UI on top of a plain <audio> element.

It targets Mantine 9 and React 19, is TypeScript-first, and has zero runtime dependencies beyond React, Mantine, and @tabler/icons-react (optional peer).

✨ Key Features

Waveform visualisation

Audio.Waveform decodes the audio file once via fetch + decodeAudioData, downsamples to waveformSamples peaks (default 512), and renders them on a <canvas>. Click or drag to seek. The played / unplayed boundary tracks the playhead at 60fps via requestAnimationFrame, not at the slower timeupdate cadence (~4Hz) — so the colour transition moves smoothly with the audio.

<Audio src="/track.mp3" variant="floating">
  <Audio.Waveform height={80} mirrorGap={0} />
  <Audio.Controls />
</Audio>
Enter fullscreen mode Exit fullscreen mode

mirror (default true) draws bars symmetrically around the centerline for the classic Audacity look. mirrorGap controls the divider thickness — 0 for a continuous filled shape, >0 for a visible centerline.

Live spectrum analyser

Mantine Audio Spectrum

Audio.Spectrum is a frequency-domain visualiser driven by Web Audio's AnalyserNode. The AudioContext is lazy-initialised on the first play (autoplay-policy compliant), and the bars react to the music in real time. Two color modes — solid (uniform fill) and gradient (vertical fade to transparent). Bin distribution is proportional: no high-frequency bins are dropped when barCount doesn't divide fftSize evenly.

<Audio src="/track.mp3" color="grape">
  <Audio.Spectrum barCount={48} colorMode="gradient" />
  <Audio.Controls />
</Audio>
Enter fullscreen mode Exit fullscreen mode

Headless useAudio hook

When the default layout doesn't fit, drop the compound layer entirely and drive your own UI:

import { useAudio } from '@gfazioli/mantine-audio';

function MyPlayer() {
  const { playing, currentTime, duration, peaks, analyser, toggle, audioRef } = useAudio({
    src: '/track.mp3',
  });

  return (
    <div>
      <audio ref={audioRef} crossOrigin="anonymous" />
      <button onClick={toggle}>{playing ? 'Pause' : 'Play'}</button>
      <span>{currentTime.toFixed(1)} / {duration.toFixed(1)}</span>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

The hook returns 16 state values (playing, currentTime, duration, volume, muted, playbackRate, peaks, analyser, audioContext, currentSrc, …) and 12 action functions (play, pause, toggle, seek, seekBy, setVolume, mute, unmute, toggleMute, setPlaybackRate, …).

scrubSound — hear the seek

Most audio players pause during a seek-drag, then jump to the new position. scrubSound keeps the audio playing during the drag — you hear short snippets of audio as the cursor moves, the way Audacity, Adobe Audition, and iTunes do it. And it works even from a paused state: the player starts on mousedown, plays through the drag, and pauses again on release.

<Audio src="/track.mp3" scrubSound>
  <Audio.Waveform height={80} />
  <Audio.Controls />
</Audio>
Enter fullscreen mode Exit fullscreen mode

The flag propagates via React context, so both Audio.Timeline and Audio.Waveform honour it automatically. Individual sub-components can still override their own scrubSound if you want fine-grained control.

Multiple sources with runtime fallback

For cross-browser compatibility, adaptive bitrate, or media-query switching, pass sources instead of src. The browser picks the first entry whose type it can play via canPlayType():

<Audio
  sources={[
    { src: '/track.aac',  type: 'audio/aac' },   // iOS preferred
    { src: '/track.ogg',  type: 'audio/ogg' },   // Firefox preferred
    { src: '/track.mp3',  type: 'audio/mpeg' },  // universal fallback
  ]}
  fallbackSrc="/last-resort.mp3"
/>
Enter fullscreen mode Exit fullscreen mode

src and sources are mutually exclusive — a dev-mode warning is logged if both are set. fallbackSrc mirrors the equivalent prop on Mantine Image and kicks in when every entry in sources errors at runtime.

asBackground preset

Turn the same player into an ambient hero / section background in one prop:

<Box pos="relative" h="100vh">
  <Audio src="/ambient.mp3" asBackground loop muted />
  <YourHeroContent />
</Box>
Enter fullscreen mode Exit fullscreen mode

controls and shortcuts are disabled by default in this mode, and a small floating mute toggle is rendered in the bottom-right corner (disable with backgroundMuteButton={false}).

Sizes & layouts

size scales 11 CSS variables at once — padding, gap, icon size, play button, action button, timeline thumb, time display width, volume slider width, font size. Five presets cover most needs:

<Audio size="xs"  src="/voice-note.mp3" variant="minimal" />  // chat bubble
<Audio size="sm"  src="/podcast.mp3"   variant="bordered" /> // card
<Audio size="lg"  src="/album.mp3"     variant="floating" /> // hero card
<Audio size="xl"  src="/master.wav"    variant="overlay"  /> // studio scrub
Enter fullscreen mode Exit fullscreen mode

And because the default <Audio.Controls /> is just a horizontal row, you can drop it (controls={false}) and arrange the sub-components however you like — vertical stack, custom skip ranges, separate volume slider section — using any layout primitive from @mantine/core.

Keyboard shortcuts

When the player has focus:

  • Space / K — play/pause
  • J / L — seek backward / forward 10s
  • ← / → — seek backward / forward 5s
  • ↑ / ↓ — volume ±5%
  • M — toggle mute
  • > / < — speed ±0.25×

🚀 Getting Started

npm install @gfazioli/mantine-audio
Enter fullscreen mode Exit fullscreen mode
import { Audio } from '@gfazioli/mantine-audio';
import '@gfazioli/mantine-audio/styles.css';

function App() {
  return <Audio src="/track.mp3" />;
}
Enter fullscreen mode Exit fullscreen mode

That's the entire setup — you now have a polished, accessible audio player with timeline, time display, mute, volume slider, and speed control.

🎨 Styles API

Audio exposes the full Mantine Styles API: every sub-component is a styles-api selector you can target via classNames or styles. Eighteen selectors in total — root, audio, controls, controlBar, playButton, timeline, timelineBuffered, timeDisplay, muteButton, skipButton, volumeSlider, speedControl, waveform, waveformCanvas, waveformHover, spectrum, spectrumCanvas, backgroundMuteButton.

Nine CSS variables let you re-skin the whole player from the outside:

.mantine-Audio-root {
  --audio-color: hotpink;
  --audio-waveform-color: rgba(255, 255, 255, 0.15);
  --audio-waveform-played-color: hotpink;
  --audio-spectrum-bar-color: hotpink;
  --audio-spectrum-bar-color-fade: transparent;
}
Enter fullscreen mode Exit fullscreen mode

Five data-* modifiers (data-variant, data-playing, data-paused, data-ended, data-as-background) are available on the root for state-based styling.

Live Demo & Documentation

The full docs site has interactive demos for every feature — usage, configurator, sizes, compound API, custom layout, waveform with a live prop configurator, spectrum with another configurator, volume & speed, asBackground, multiple sources, headless usage, keyboard shortcuts, styles API, and a "use cases" showcase with real-world layouts (inline voice-note bubble, podcast card with cover, mini sticky bar, studio scrub).

https://gfazioli.github.io/mantine-audio

Links

Top comments (0)