Add sound effects to a React app
Free, CC0, no API key. Fetch one coherent set of sounds, paste its URL map into a tiny play() module, hotlink the audio. Ten minutes, no build step, no accounts.
1. Fetch a sound set
A product needs a handful of cues that sound like they belong together, not one click. Sets give you exactly that: one sound per role, picked by measured acoustics. For a work tool use saas-app; for a consumer or chat app chat-app or ui-soft; for a store checkout; for an AI agent or CLI ai-coding-tool. All 15 are listed at /sets.
curl "https://sfxmint.com/api/v1/sets/saas-app"The response (trimmed) — the part you want is urls:
{
"id": "saas-app",
"style": "crisp",
"license": "CC0-1.0",
"sounds": {
"click": {
"slug": "ui-click-30",
"duration_ms": 1000,
"acoustics": { "attack_ms": 0, "tail_ms": 40, "centroid_hz": 4759, "character": ["bright", "punchy", "tight"] },
"mp3_url": "https://sfxmint.com/dl/ui-click-30.mp3",
"wav_url": "https://sfxmint.com/dl/ui-click-30.wav",
"alternates": ["ui-click-10", "ui-click-27"]
}
},
"urls": {
"click": "https://sfxmint.com/dl/ui-click-30.mp3",
"hover": "https://sfxmint.com/dl/ui-hover-02.mp3",
"toggle": "https://sfxmint.com/dl/ui-toggle-09.mp3",
"save": "https://sfxmint.com/dl/feedback-success-22.mp3",
"error": "https://sfxmint.com/dl/feedback-error-34.mp3",
"notification": "https://sfxmint.com/dl/ui-chime-14.mp3"
}
}Picks can change as the library grows; the URLs you paste never do. Prefer a different variant? Every role lists two alternates — swap the slug.
2. A zero-dependency play() module
One module, one Audio element per sound, cached after the first play. No library needed for UI cues.
// src/sfx.ts — paste the "urls" map from the set response
const URLS = {
click: "https://sfxmint.com/dl/ui-click-30.mp3",
save: "https://sfxmint.com/dl/feedback-success-22.mp3",
error: "https://sfxmint.com/dl/feedback-error-34.mp3",
notification: "https://sfxmint.com/dl/ui-chime-14.mp3",
} as const;
export type Sfx = keyof typeof URLS;
const cache = new Map<Sfx, HTMLAudioElement>();
let muted = false;
export function setMuted(m: boolean) {
muted = m;
}
export function play(name: Sfx, volume = 0.5) {
if (muted || typeof window === "undefined") return; // SSR-safe
let a = cache.get(name);
if (!a) {
a = new Audio(URLS[name]);
a.preload = "auto";
cache.set(name, a);
}
a.currentTime = 0; // retrigger cleanly on rapid clicks
a.volume = volume;
a.play().catch(() => {}); // rejected until the first user gesture — that's fine
}import { play } from "./sfx";
export function SaveButton({ onSave }: { onSave: () => Promise<void> }) {
return (
<button
onClick={async () => {
play("click");
try {
await onSave();
play("save");
} catch {
play("error");
}
}}
>
Save
</button>
);
}3. Already using use-sound or Howler?
Same URLs, one line each. Both work with cross-origin files because the audio is served with CORS *.
import useSound from "use-sound";
const [playClick] = useSound("https://sfxmint.com/dl/ui-click-30.mp3", { volume: 0.5 });
<button onClick={() => playClick()}>Save</button>import { Howl } from "howler";
const click = new Howl({ src: ["https://sfxmint.com/dl/ui-click-30.mp3"], volume: 0.5 });
click.play();4. Hotlinking: what you can rely on
https://sfxmint.com/dl/{slug}.mp3and.wavare permanent. A slug is never reused or re-encoded.- Served with
Cache-Control: immutableandAccess-Control-Allow-Origin: *— browsers cache them for a year, Web Audio andfetchcan read them from any origin. - No API key, no referer check, no rate limit on playback. CC0, so no attribution.
- Never append query parameters to
/dl/URLs. If you need offline builds, copy the files intopublic/sfx/— that is allowed too. - Sizes: a 1-second UI cue is roughly 15 KB as MP3. Use WAV only for seamless loops.
5. One-off sounds: roles and search
Need a cue that is not in your set? Ask by role — what the sound is for — and you get one QA-checked default instead of a text-search near miss:
curl "https://sfxmint.com/api/v1/roles/purchase-success"
# → { "role": "purchase-success", "slug": "...", "mp3_url": "...", "wav_url": "...",
# "loopable": false, "acoustics": {...}, "alternates": [ { ...same shape } ], "family_url": "..." }
curl "https://sfxmint.com/api/v1/roles" # every role with aliases
curl "https://sfxmint.com/api/v1/search?q=camera+shutter&limit=5" # free textAliases work (button-click, ka-ching style phrases map to the same role); add ?style=crisp|soft|spacious to re-rank the family by measured acoustics. Search results carry match: role and exact are reliable, semantic is the nearest thing by meaning — listen before shipping those. The human-readable index is at /roles.
6. UX rules that keep users from muting your app
- Play only after a user gesture. Browsers block audio until the user has clicked, tapped or typed;
play()rejects withNotAllowedError. Never play on mount, on route change or on a timer before the first interaction. - Ship a mute toggle and remember it in
localStorage. Wire it tosetMuted()above. - Keep volume at or below 0.6 — 0.3–0.5 for UI cues. Loud clicks feel like bugs.
- One sound per event, cues under 300 ms, no hover sounds on touch devices, never on every keystroke.
- Preload the three to five sounds you use most; leave the rest lazy.
License
Every SFXMint sound is CC0 1.0: commercial use, no attribution, no signup. Full terms at /license.
Using Claude Code or Cursor?
Install the skill and your agent learns this exact workflow — role first, then set, then search — plus the autoplay and volume rules:
npx skills add flreey/sfxmint-mcp -yPrefer MCP? claude mcp add --transport http sfxmint https://sfxmint.com/mcp gives the agent get_role_sound, get_sound_set and search_sounds natively. Full reference: API docs.