State & Feedback Systems

UI Sound Kit

Lightweight interface sounds — click, hover, success, error and more — synthesized live with Web Audio, with built-in volume control, a global switch, and preference-respecting behavior; no audio files, no extra requests.

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 ui-sound-kit.en

New project or installation trouble? Start here →

UI Sound Kit is a React UI sound effects kit that synthesizes click, notification, success, and error sounds on the fly with the browser's Web Audio API. It exposes a useSound() hook through React Context, and its bundled buttons are styled with Tailwind CSS.

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

0.5
Active edited usageDefault props
<SoundButton/>

The complete example includes the preview layout and data. Install the component first, then paste this file into your project.

When to use

  • Feedback sounds for key actions such as form submission, completed payments, or failed uploads, so the result is easier to notice.
  • Notification sounds when a new message arrives in chat, support, or collaboration tools.
  • Button sounds in gamified interfaces, interactive onboarding, or educational products for kids.
  • A "UI sounds" toggle and volume control on a settings page, with the preference saved in the browser automatically.
  • Not a good fit: playing your own audio files or background music. The kit ships six fixed synthesized sounds and has no API for loading audio files.

Installation

npx webberui@latest add https://webberui.com/r/ui-sound-kit.en.json

Or, once registries are configured in components.json, install it as @webberui/ui-sound-kit.

Usage

Wrap SoundProvider once around the scope that needs sound, and any component inside it can get play() plus the switch/volume controls through useSound():

import {
  SoundProvider,
  useSound,
} from "@/components/ui/ui-sound-kit";

function App() {
  return (
    <SoundProvider defaultVolume={0.5}>
      <SaveButton />
    </SoundProvider>
  );
}

function SaveButton() {
  const { play } = useSound();
  return (
    <button
      onClick={async () => {
        try {
          await save();
          play("success");
        } catch {
          play("error");
        }
      }}
    >
      Save
    </button>
  );
}

Built-in sound names: "click", "hover", "success", "error", "toggle", "notification" (the full list is available from SOUND_NAMES).

Built-in components

SoundButton is a button that makes sound, playing automatically on click (and optionally on hover); SoundToggle is the global switch button:

import {
  SoundButton,
  SoundToggle,
} from "@/components/ui/ui-sound-kit";

<SoundButton sound="click" hoverSound="hover">
  Play
</SoundButton>

<SoundToggle />

Examples

Mount the provider in the Next.js root layout

SoundProvider is a client component, so you can place it directly in a Server Component app/layout.tsx and wrap children.

app/layout.tsx
import { SoundProvider } from "@/components/ui/ui-sound-kit";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <SoundProvider defaultVolume={0.4}>{children}</SoundProvider>
      </body>
    </html>
  );
}

Toggle and volume slider on a settings page

Connect a native range input to volume and setVolume from useSound(), and let SoundToggle handle the global on/off switch.

"use client";

import { SoundToggle, useSound } from "@/components/ui/ui-sound-kit";

export function SoundSettings() {
  const { volume, setVolume, enabled } = useSound();

  return (
    <div className="flex items-center gap-3">
      <SoundToggle />
      <input
        type="range"
        min={0}
        max={1}
        step={0.05}
        value={volume}
        disabled={!enabled}
        onChange={(e) => setVolume(Number(e.target.value))}
        aria-label="UI sound volume"
      />
    </div>
  );
}

Play a notification sound on new messages

The second argument to play() sets a volume multiplier for that one playback, applied on top of the global volume.

"use client";

import * as React from "react";
import { useSound } from "@/components/ui/ui-sound-kit";

export function useMessageSound(socket: WebSocket) {
  const { play } = useSound();

  React.useEffect(() => {
    const onMessage = () => play("notification", { volume: 0.6 });
    socket.addEventListener("message", onMessage);
    return () => socket.removeEventListener("message", onMessage);
  }, [socket, play]);
}

Props

SoundProvider

PropTypeDefaultDescription
defaultEnabledbooleantrueWhether sound starts enabled (overridden after mount when a persisted preference exists)
defaultVolumenumber0.5Initial global volume, 0 to 1
storageKeystring | null"wb-sound-kit"localStorage key that persists the switch and volume; null disables persistence
respectReducedMotionbooleantrueWhether to also mute when the system has "reduce motion" enabled

What useSound() returns

NameTypeDescription
play(name, options?) => voidSynthesizes and plays a built-in sound; options.volume is a one-off volume multiplier
enabledbooleanWhether global sound is on
setEnabled(enabled: boolean) => voidTurn global sound on/off
volumenumberGlobal volume, 0 to 1
setVolume(volume: number) => voidSet the global volume (automatically clamped to 0–1)

SoundButton

PropTypeDefaultDescription
soundSoundName"click"The sound played on click
hoverSoundSoundName—When set, this sound also plays as the cursor moves in

All other native button attributes are forwarded. SoundToggle forwards native button attributes too, and uses chime (default true) to control whether a confirmation sound plays when it is switched on.

How it works

  • Synthesized live, zero assets: sounds are synthesized on the fly with Web Audio's OscillatorNode + GainNode — no audio file is loaded and no extra request is made.
  • Lazy initialization: the AudioContext is created and auto-resumed only on the first play() (usually from a user click gesture), which complies with browser autoplay policies; it also tries to recover if it was suspended while the tab was in the background.
  • A single audio graph: the whole Provider shares one AudioContext and one master gain node; every playback attaches its own bus gain, applies the one-off volume multiplier, and is then recycled.
  • Persisted preferences: the switch and volume are written to localStorage by default and carried across refreshes; turn this off with storageKey={null}.

Accessibility

  • SoundToggle expresses its state with aria-pressed, and switches its aria-label (enable sound/disable sound) and icon along with the state.
  • The volume slider is a native <input type="range"> with an aria-label, operable by keyboard.
  • When the user has "reduce motion" enabled at the system level, respectReducedMotion mutes by default, respecting the preference for less stimulation; with sound off, interaction behavior (onClick and so on) is unaffected — it simply makes no sound.

FAQ

Does it work with the Next.js App Router?

Yes. The source already has "use client", and SoundProvider can sit directly inside a Server Component layout. useSound(), SoundButton, and SoundToggle must all be used inside a SoundProvider; otherwise useSound() throws an error.

Why is there no sound the first time after the page loads?

Browser autoplay policies require audio to start from a user gesture. The AudioContext is only created, and resumed if needed, on the first play() call, so call it from an event such as a click rather than automatically on page load.

Do I need audio files?

No. Every sound is synthesized in real time with OscillatorNode and GainNode, with no audio files and no extra requests. The only dependencies are motion (to detect reduced motion) and lucide-react (toggle icons).

Is the user's preference remembered?

Yes. The on/off state and volume are saved to localStorage under the "wb-sound-kit" key by default; change the key with storageKey, or pass null to disable saving.

Does it play sounds when reduced motion is on?

Not by default. respectReducedMotion defaults to true, so play() stays silent when the system's reduced-motion setting is on, while handlers such as onClick still run. Pass respectReducedMotion={false} to turn this off.