npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

clicktone

v3.0.2

Published

A lightweight helper for UI sound feedback. It wraps the Web Audio API with a tiny, event-based API and a rock-solid iOS/Safari unlock.

Readme

clicktone

A lightweight helper for UI sound feedback. It wraps the Web Audio API with a tiny, event-based API and a rock-solid iOS/Safari unlock.

npm NPM Downloads

Demo


Features

  • Single shared AudioContext for every instance (no iOS context-limit issues).
  • Reliable autoplay unlock: eager gesture listeners, synchronous resume, silent priming ping, re-resume on visibilitychange.
  • Self-healing on iOS/Safari: detects a "zombie" context after tab/app switch or lock-screen and transparently rebuilds it on the next interaction.
  • Automatic format fallback via canPlayType (great for Safari, which dislikes OGG/Opus).
  • Shared decode cache, throttling, volume control, loop playback, stop control, and subtle pitch variation.
  • Event-based (EventTarget) with a typed on() / once() API; play() also returns a Promise.

Installation

npm install clicktone

Quick Start

import { ClickTone } from 'clicktone';

const button = document.getElementById('clickBtn');

if (!button) {
  throw new Error('Demo DOM is not ready');
}

const sound = new ClickTone({
  src: new URL('./assets/click.mp3', import.meta.url).href,
  volume: 0.7,
  throttle: 100,
  pitchVariation: 0.08,
  preload: true,
});

sound.on('error', (error) => console.error(error));
button.addEventListener('click', () => sound.play());

The src option also accepts DOM elements: an HTMLSourceElement, an HTMLAudioElement (its child <source> elements become the fallback list), or { id } referencing either of those by id.

Pass an array to enable automatic format fallback:

const sound = new ClickTone({
  src: ['./click.aac', './click.ogg', './click.mp3'],
});

API

  • new ClickTone(options) — creates a sound instance bound to the shared audio engine.
  • sound.play(srcOrOptions?) — starts playback; returns a Promise that resolves when playback ends (or when a loop starts).
  • sound.preload(src?) — fetches and decodes the buffer ahead of time.
  • sound.stop() — stops active playback on this instance.
  • sound.unlock() — resumes the shared AudioContext inside a user gesture.
  • sound.destroy() — stops playback and removes listeners for this instance.
  • sound.on(type, fn) / sound.once(type, fn) — typed event subscriptions; return an unsubscribe function.
  • ClickTone.unlock() — static helper to unlock the shared context from a global gesture handler.
  • ClickTone.unlocked — whether the shared context is currently unlocked.

Options

| Option | Type | Default | Description | | --- | --- | --- | --- | | src | string \| URL \| {src,type?} \| {id} \| HTMLSourceElement \| HTMLAudioElement \| Array<…> | — | Audio source(s). Pass an array to enable automatic format fallback. | | volume | number | 1 | Playback volume 01. | | muted | boolean | false | Start muted. Toggle later via setMuted() / toggleMuted(). | | throttle | number | 0 | Debounce interval in ms. Requests arriving sooner are ignored. | | pitchVariation | number | 0 | 01. Random playback-rate jitter so repeated clicks don't sound identical. | | preload | boolean | false | Fetch and decode the buffer on construction. | | loop | boolean | false | Keep playback running until stop() is called. | | replay | 'overlap' \| 'interrupt' \| 'ignore-if-playing' \| 'restart' | 'overlap' | Behavior when play() is called while this instance is already playing. | | debug | boolean | false | Log internal errors to the console (in addition to the error event). |


Events

ClickTone extends EventTarget. Subscribe with the typed on() / once() helpers or with native addEventListener.

| Event | Detail | Fired when | | --- | --- | --- | | play | — | A buffer source has started. | | end | — | Playback finished naturally. | | stop | — | Active playback was stopped explicitly. | | unlock | — | The shared AudioContext became playable. | | load | AudioBuffer | A source was fetched and decoded. | | error | Error | Loading, decoding, or playback failed. |

const off = sound.on('end', () => console.log('finished'));
sound.on('error', (error) => console.error(error));
sound.once('unlock', () => console.log('audio unlocked'));

off();

await sound.play();

Methods

await sound.play();
await sound.play({ loop: true });
await sound.play({ src: '/win.mp3', replay: 'interrupt' });
await sound.preload();
sound.unlock();
sound.stop();
sound.playing;
sound.setVolume(0.5);
sound.setMuted(true);
sound.toggleMuted();
sound.on('end', () => {});
sound.destroy();

ClickTone.unlock();
ClickTone.unlocked;

Replay behavior

  • overlap (default): preserve classic SFX behavior — every accepted play() call starts another buffer source.
  • interrupt: stop active playback, then start the requested sound.
  • ignore-if-playing: ignore the request while this instance is already playing.
  • restart: stop active playback and start again, bypassing throttle.

For looped playback, play() resolves after the loop starts rather than waiting forever for a natural end. Use stop() to end it.


Browser support

Mobile browsers (especially Safari on iOS) block audio until the user interacts with the page. An AudioContext must be resumed synchronously inside a user gesture, and iOS caps the number of contexts you may create. ClickTone handles all of this:

  • One shared AudioContext (with latencyHint: 'interactive') — one unlock unlocks every instance.
  • Global gesture listeners (pointerdown / pointerup / touchstart / touchend / keydown) resume the context on the first interaction anywhere on the page.
  • A silent priming ping is played right after unlock, and the context is re-resumed on visibilitychange.
  • Lifecycle recovery for Safari/iOS: after a tab switch, app switch, or lock-screen, WebKit can leave the context in a "zombie" state (it reports running but stays silent). ClickTone probes for this on return and, if the context is dead, rebuilds it inside the next user gesture — buffers re-decode and the gain graph rebinds automatically.
  • Decoded buffers are cached and shared, so the first click is not delayed by the network.

Pass preload: true (or call sound.preload()) so the buffer is decoded ahead of the first click. Include an MP3/AAC source for Safari — OGG/Opus is not reliably supported there.

await sound.preload();

document.addEventListener('pointerdown', () => ClickTone.unlock(), { once: true });

Vue 3

Use ClickTone directly from the main entry — the package stays framework-agnostic.

<script setup lang="ts">
import { onBeforeUnmount, onMounted, shallowRef } from 'vue';
import { ClickTone } from 'clicktone';

const sound = shallowRef<ClickTone | null>(null);

onMounted(() => {
  sound.value = new ClickTone({
    src: new URL('./assets/click.mp3', import.meta.url).href,
    volume: 0.7,
    pitchVariation: 0.08,
    preload: true,
  });
});

onBeforeUnmount(() => {
  sound.value?.destroy();
  sound.value = null;
});
</script>

<template>
  <button @click="sound?.play()">Click</button>
</template>

Advanced usage

Use one ClickTone instance for a long-running process sound and separate instances for terminal SFX:

const flying = new ClickTone({
  src: new URL('./assets/flying.mp3', import.meta.url).href,
  loop: true,
  preload: true,
});

const bonus = new ClickTone({
  src: new URL('./assets/bonus.mp3', import.meta.url).href,
  preload: true,
});

await flying.play();

flying.stop();
await bonus.play();

Encode the interruption policy on a single instance:

const terminal = new ClickTone({
  src: new URL('./assets/explosion.mp3', import.meta.url).href,
  replay: 'interrupt',
  preload: true,
});

await terminal.play();

License

MIT