Mantine Audio

Logo

@gfazioli/mantine-audio

A Mantine-native audio player for React with waveform visualization and live spectrum analyzer, built on Web Audio API. Compound component API + headless useAudio hook.

Installation

yarn add @gfazioli/mantine-audio

After installation import package styles at the root of your application:

import '@gfazioli/mantine-audio/styles.css';

You can import styles within a layer @layer mantine-audio by importing @gfazioli/mantine-audio/styles.layer.css.

import '@gfazioli/mantine-audio/styles.layer.css';

Overview

@gfazioli/mantine-audio is a Mantine-native audio player for React with waveform visualisation and a live spectrum analyser, built on the Web Audio API. It ships as three layers in one package:

  • A polished default <Audio /> component with four built-in variants (overlay, minimal, floating, bordered)
  • 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 that returns state, actions, the decoded peaks and the AnalyserNode for building 100% custom UIs

Usage

Drop a single line and you have a styled, accessible audio player wired to the native <audio> element:

0:00 / 0:00

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

function Demo() {
  return <Audio src="/audio/showcase.mp3" />;
}

Configurator

Explore the available props in isolation:

0:00 / 0:00

Color
Radius
import { Audio } from '@gfazioli/mantine-audio';

function Demo() {
  return (
    <Audio src="/audio/showcase.mp3"/>
  );
}

Sizes

The size prop scales the entire player — padding, icon size, play button, timeline thumb, time display width, volume slider width, and font size. Available presets: xs, sm, md (default), lg, xl. Pair it with variant for fine layout control.

size = xs

0:00 / 0:00

size = sm

0:00 / 0:00

size = md

0:00 / 0:00

size = lg

0:00 / 0:00

size = xl

0:00 / 0:00

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

function Demo() {
  return (
    <>
      <Audio size="xs" src="/audio/showcase.mp3" />
      <Audio size="sm" src="/audio/showcase.mp3" />
      <Audio size="md" src="/audio/showcase.mp3" />
      <Audio size="lg" src="/audio/showcase.mp3" />
      <Audio size="xl" src="/audio/showcase.mp3" />
    </>
  );
}

Compound API

The default control bar is <Audio.Controls>. Replace it with any combination of sub-components — they all read state from the shared context provided by the parent <Audio>:

<Audio src="...">
  <Audio.Waveform height={80} />
  <Audio.Controls>
    <Audio.SkipButton seconds={-15} />
    <Audio.PlayButton />
    <Audio.SkipButton seconds={15} />
    <Audio.Timeline />
    <Audio.TimeDisplay />
    <Audio.MuteButton />
    <Audio.VolumeSlider />
    <Audio.SpeedControl />
  </Audio.Controls>
  <Audio.Spectrum barCount={48} colorMode="gradient" />
</Audio>

Custom layout

The default <Audio.Controls /> is just a horizontal row. Because every sub-component is a free-standing piece reading state from the shared context, you can drop the default control bar (controls={false}) and arrange the parts however you want — reorder, group, stack vertically, mix with any layout primitive from @mantine/core. No new prop required.

1. Default

0:00 / 0:00

2. Two-row (waveform on top, controls below)

0:00 / 0:00

3. Vertical (manual layout with Stack + Group)

0:00 / 0:00

import { Audio } from '@gfazioli/mantine-audio';
import { Group, Stack } from '@mantine/core';

// 1. Default — the built-in <Audio.Controls /> renders a horizontal row.
<Audio src="/track.mp3" />

// 2. Two-row — waveform on top, controls below.
<Audio src="/track.mp3">
  <Audio.Waveform height={48} />
  <Audio.Controls />
</Audio>

// 3. Vertical — every sub-component on its own row, controls grouped manually.
<Audio src="/track.mp3" controls={false}>
  <Stack gap="xs">
    <Audio.Waveform height={48} />
    <Audio.Timeline />
    <Group justify="space-between" wrap="nowrap">
      <Group gap="xs">
        <Audio.SkipButton seconds={-10} />
        <Audio.PlayButton />
        <Audio.SkipButton seconds={10} />
      </Group>
      <Audio.TimeDisplay />
      <Group gap="xs">
        <Audio.MuteButton />
        <Audio.VolumeSlider />
        <Audio.SpeedControl />
      </Group>
    </Group>
  </Stack>
</Audio>

Waveform

Audio.Waveform renders the audio file's peaks on a <canvas>. The peaks are decoded once via fetch + decodeAudioData and downsampled to waveformSamples (default 512). Click or drag on the waveform to seek. The played/unplayed boundary tracks the playhead at 60fps via requestAnimationFrame, not at the slower timeupdate cadence.

0:00 / 0:00

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

function Demo() {
  return (
    <Audio src="/audio/topcat.mp3" variant="floating">
      <Audio.Waveform height={80} />
      <Audio.Controls />
    </Audio>
  );
}

Remote files must be served with permissive CORS headers (Access-Control-Allow-Origin) for the decode to succeed. When the decode fails the waveform degrades to an empty bar — the rest of the player keeps working.

Play with the available props live:

0:00 / 0:00

Height
Bar gap
Bar radius
Color
Mirror gap
import { Audio } from '@gfazioli/mantine-audio';

function Demo() {
  return (
    <Audio src="/audio/topcat.mp3" variant="floating"{{audioProps}}>
      <Audio.Waveform{{waveformProps}} />
      <Audio.Controls />
    </Audio>
  );
}

Spectrum

Audio.Spectrum is a live frequency-domain visualiser driven by Web Audio's AnalyserNode. The AudioContext is lazy-initialised on the first play to comply with browsers' autoplay policies. Hit play and the bars react to the music in real time.

0:00 / 0:00

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

function Demo() {
  return (
    <Audio src="/audio/mozart.mp3" variant="floating" color="grape">
      <Audio.Spectrum height={80} barCount={48} colorMode="gradient" />
      <Audio.Controls />
    </Audio>
  );
}

Set fftSize on the parent <Audio> to tune resolution (must be a power of two between 32 and 32768; default 256). Set colorMode="gradient" for a vertical gradient from --audio-spectrum-bar-color (top) to --audio-spectrum-bar-color-fade (bottom, default transparent). Set mirror to render the bars symmetrically around the vertical center for a classic equalizer look.

0:00 / 0:00

Height
Bar count
Bar gap
Bar radius
Smoothing
Color
import { Audio } from '@gfazioli/mantine-audio';

function Demo() {
  return (
    <Audio src="/audio/mozart.mp3" variant="floating" color="grape">
      <Audio.Spectrum height={80} barCount={48} color="grape" colorMode="gradient" />
      <Audio.Controls />
    </Audio>
  );
}

Volume & Speed

Audio.VolumeSlider is a 0–100% Mantine Slider bound to volume. Audio.SpeedControl opens a menu with the configured preset speeds (default [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2]).

asBackground

Use the same player as an ambient background track in a hero section:

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

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

Multiple sources

For cross-browser compatibility, adaptive bitrate, or media-query-driven switching, use the sources prop 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
]} />

Adaptive bitrate via HLS (Safari native, others via hls.js attached to src):

<Audio sources={[
  { src: '/stream.m3u8', type: 'application/vnd.apple.mpegurl' },
  { src: '/fallback.mp3', type: 'audio/mpeg' },
]} />

Media-query-based switching (e.g. lower bitrate on mobile):

<Audio sources={[
  { src: '/hi.mp3', type: 'audio/mpeg', media: '(min-width: 1024px)' },
  { src: '/lo.mp3', type: 'audio/mpeg' },
]} />

src and sources are mutually exclusive — if both are set, sources wins and a dev-mode warning is logged.

Runtime fallback

If every entry in src / sources fails to load at runtime (the <audio> element fires its error event — typically a 404 or decode error), the component swaps to fallbackSrc. This mirrors the equivalent prop on Mantine Image.

<Audio
  sources={[
    { src: '/primary.aac', type: 'audio/aac' },
    { src: '/primary.mp3', type: 'audio/mpeg' },
  ]}
  fallbackSrc="/last-resort.mp3"
/>

Note on waveform with sources

Audio.Waveform decodes peaks from the URL the browser actually picked (audioRef.currentSrc, available after loadedmetadata). For HLS / DASH streams, the decode bypasses the streaming layer and may not work — the rest of the player keeps working.

Headless usage

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

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

function MyPlayer() {
  const { playing, currentTime, duration, peaks, analyser, toggle, seek, 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>
  );
}

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

Keyboard shortcuts

When shortcuts is true (default) and the player is focused:

KeyAction
Space / KPlay / pause
J / Seek backward (10s / 5s)
L / Seek forward (10s / 5s)
/ Volume +/− 5%
MToggle mute
> / .Speed +0.25×
< / ,Speed −0.25×

Styles API

Audio supports the Mantine Styles API. You can target every part of the compound (root, controls, controlBar, playButton, timeline, waveform, spectrum, …) with classNames and styles props, or override CSS variables with vars.

0:00 / 0:00

Component Styles API

Hover over selectors to highlight corresponding elements

/*
 * Hover over selectors to apply outline styles
 *
 */

Use cases

Real-world layouts assembled from size + variant + compound sub-components:

1. Inline message (chat / voice note)

0:00 / 0:00

2. Podcast card (cover + waveform)

🎙️

The Mantine Show — Episode #42

Custom components without losing your mind

0:00 / 0:00

3. Mini sticky bar (variant=minimal)

0:00 / 0:00

4. Studio scrub (large waveform + scrubSound)

0:00 / 0:00

import { Audio } from '@gfazioli/mantine-audio';
import { Avatar, Group, Stack, Text } from '@mantine/core';

// 1. Inline chat message
<Audio size="xs" variant="minimal" src="/voice-message.mp3" />

// 2. Card with cover + waveform (podcast)
<Group wrap="nowrap" align="flex-start">
  <Avatar src="/cover.jpg" size={80} radius="md" />
  <Stack gap={4} style={{ flex: 1 }}>
    <Text fw={600}>Episode title</Text>
    <Text fz="xs" c="dimmed">Podcast name · 32:14</Text>
    <Audio size="sm" src="/episode.mp3">
      <Audio.Waveform height={48} />
      <Audio.Controls />
    </Audio>
  </Stack>
</Group>

// 3. Sticky mini player (place inside a fixed positioned wrapper)
<Audio size="sm" variant="minimal" src="/track.mp3" />

// 4. Studio scrub with big waveform + scrubSound
<Audio size="xl" src="/track.mp3" scrubSound>
  <Audio.Waveform height={120} mirrorGap={2} />
  <Audio.Controls />
</Audio>