Think of the apps that feel the most polished. Many of them make a small sound when a payment goes through, a message is sent or a switch flips. You don't notice the sound. You'd notice if it was missing.
On the web, most of us skip it. Adding sounds usually means finding .mp3 files, hosting them, preloading them, and dealing with browsers that block audio before the user interacts with the page.
tonely takes a different route. It doesn't ship audio files at all. It builds each sound when it plays, from oscillators and envelopes in the Web Audio API. The whole library is under 3 kB gzip and makes no network requests.
The basics
import { tonely } from "tonely";
tonely.success();
tonely.error();
tonely.click();
tonely.toggle(true); // rises for on, falls for off
The built-in sounds are success, error, warning, info, click, pop, toggle, delete, glass and bubble. There are five themes (modern, glass, minimal, playful and retro), and every call takes volume, pitch, playbackRate and theme:
tonely.setTheme("glass");
tonely.success({ volume: 0.6, pitch: 1.1 });
Sounds that follow your API
Most UI feedback is really "did that request work?". tonely can answer that from the HTTP status:
const res = await fetch("/api/orders", { method: "POST" });
tonely.fromStatus(res.status); // 2xx success, 3xx info, 4xx and 5xx error
Or wrap fetch once and forget about it:
const fetchWithSound = tonely.wrapFetch();
await fetchWithSound("/api/checkout");
If you already have a promise, tonely.track(promise) plays a sound when it settles and returns the same promise.
React and Next.js
"use client";
import { useTonely } from "tonely/react";
export function PayButton() {
const { click, success, error } = useTonely({ theme: "glass" });
return (
<button
onClick={async () => {
click();
try {
await pay();
success();
} catch {
error();
}
}}
>
Pay
</button>
);
}
On the server, every call is a safe no-op, so importing tonely in shared code won't break SSR. Version 0.1.1 also ships tonely/react with a "use client" directive, so it works in App Router files without extra setup.
Toasts with sound
If you use Sonner or shadcn/ui toasts, a small wrapper gives every toast a matching sound:
import { toast as base } from "sonner";
import { tonely } from "tonely";
export const toast = {
success: (m: string) => { tonely.success(); return base.success(m); },
error: (m: string) => { tonely.error(); return base.error(m); },
};
Use sound with care
Sound is easy to overdo. A few rules I follow:
- Play sounds for outcomes (saved, paid, failed), not for every hover or keystroke.
- Always offer a mute switch.
tonely.setMuted(true)silences everything, so it's one line to wire up to a setting. - Never use sound as the only feedback. Someone with the sound off still needs to see that it worked.
- Browsers only allow audio after the user has interacted with the page, so trigger sounds from event handlers.
Try it
npm install tonely
You can hear every sound and theme on the demo site. It works with React, Next.js, Vue, Svelte or plain JavaScript. Ideas for new sounds are welcome on GitHub.
Top comments (1)
Good rules at the end, especially never using sound as the only feedback. One edge for wrapFetch and track: an aborted request (AbortController, a route change mid-flight) rejects with AbortError, not an HTTP status, so it shouldn't play the error sound. Worth skipping AbortError so cancelling a search doesn't beep at the user.