Skip to content
Coastal Healthcare Advocates

Module: sound

A soft, synthesised click sound when a button is pressed.

How the sound is made

Generated live with the Web Audio API, so there is no audio file to download. Each click layers two short sounds, like tapping a wooden key:

  • Tick — a very short burst of white noise through a band-pass filter (the crisp transient at the start).
  • Body — a quick sine tone whose pitch falls slightly (the soft "tock").

The pitch and filter frequency drift by a few percent on every press, so repeated clicks feel played rather than looped.

When it plays

On the click event, so mouse, touch and keyboard (Enter / Space) presses all make a sound. It covers <button>, .btn links, [role="button"], submit/button <input>s and <summary> (see TARGETS).

It stays silent for:

  • disabled controls (disabled or aria-disabled="true");
  • clicks fired by script (element.click()), which aren't "trusted";
  • visitors who prefer reduced motion, or who turned Animations off in Site preferences (data-motion="off" on <html>);
  • anyone with localStorage.sound === 'off' (the hook for a future on/off control).

Browsers only allow audio to start after a user gesture, so the AudioContext is created on the first press. It's created/resumed during the press (queued from pointerdown, for just after the pressed state has painted) so the context is already running by the time click fires on release — otherwise the very first click would sound late.

Loading

A classic <script defer> in <head>, beside email.js. ES5 inside an IIFE so nothing leaks globally.

Source:

Members

(inner) AC :function|undefined

The Web Audio constructor (with the old Safari prefix as a fallback).

Type:
  • function | undefined
Source:

(inner) TARGETS :string

CSS selector for the elements that make a sound when clicked.

Type:
  • string
Source:

(inner) VOLUME :number

Master output gain (0–1).

Type:
  • number
Source:

(inner) ctx :AudioContext|null

The shared audio context, created lazily by setup.

Type:
  • AudioContext | null
Source:

(inner) lastAt :number

performance.now() timestamp of the last click that played a sound, used to debounce double-triggers.

Type:
  • number
Source:

(inner) noise :AudioBuffer|null

Reusable 60 ms mono buffer of white noise for the "tick".

Type:
  • AudioBuffer | null
Source:

(inner) out :GainNode|null

Master gain node every voice connects to (→ compressor → speakers).

Type:
  • GainNode | null
Source:

(inner) reduce :MediaQueryList

Live media query for the visitor's reduced-motion preference.

Type:
  • MediaQueryList
Source:

(inner) warming :boolean

Whether a deferred setup is already queued (see warm).

Type:
  • boolean
Source:

Methods

(inner) envelope(param, t, peak, attack, decay) → {void}

Schedules a percussive attack/decay envelope on an audio parameter.

Exponential ramps can't start from or reach exactly 0, so 0.0001 (effectively silent) is used as the floor.

Parameters:
Name Type Description
param AudioParam

The parameter to shape (usually a gain).

t number

Start time, in the context's clock (seconds).

peak number

Value to rise to.

attack number

Seconds to rise from silence to peak.

decay number

Seconds to fall from peak back to silence.

Source:
Returns:
Type
void

(inner) muted() → {boolean}

Whether sound should be suppressed right now.

Source:
Returns:

true if the visitor prefers reduced motion or has turned sound off via localStorage.sound = 'off'.

Type
boolean

(inner) play() → {void}

Plays a click, first resuming the audio context if the browser has it suspended. Failures (e.g. autoplay policy) are ignored silently.

Source:
Returns:
Type
void

(inner) setup() → {AudioContext|null}

Creates the audio context and the shared audio graph the first time it's needed; afterwards just returns the existing context.

Graph: voice nodes → out (gain) → compressor → speakers.

Source:
Returns:

The context, or null if the browser refused to create one.

Type
AudioContext | null

(inner) target(e) → {Element|null}

Finds the clickable control an event came from, if it's one that should make a sound.

Uses closest() so a click on an icon or <span> inside a button still counts as a click on the button.

Parameters:
Name Type Description
e Event

A pointerdown or click event.

Source:
Returns:

The matching, enabled control, or null.

Type
Element | null

(inner) voice(c) → {void}

Plays one click: a filtered-noise "tick" layered over a falling sine "body". All nodes are one-shot and are discarded after they stop.

Parameters:
Name Type Description
c AudioContext

A running audio context.

Source:
Returns:
Type
void

(inner) warm() → {void}

Gets the context ready during a press, without holding the press up.

The first new AudioContext() in a browser session is synchronous and slow: it waits on the audio device, not the CPU (200–500 ms in Chromium, measured 2026-09-18). Run inside the pointerdown handler, that wait sat between the visitor's first press and anything on screen answering it. So the first build is queued for just after the next paint: the pressed state shows at once, and the context is built while the finger is still down. If the click gets there first, play builds it as before.

Source:
Returns:
Type
void