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 (
disabledoraria-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.
Members
(inner) AC :function|undefined
The Web Audio constructor (with the old Safari prefix as a fallback).
Type:
- function | undefined
(inner) TARGETS :string
CSS selector for the elements that make a sound when clicked.
Type:
- string
(inner) VOLUME :number
Master output gain (0–1).
Type:
- number
(inner) ctx :AudioContext|null
The shared audio context, created lazily by setup.
Type:
- AudioContext | null
(inner) lastAt :number
performance.now() timestamp of the last click that played a sound,
used to debounce double-triggers.
Type:
- number
(inner) noise :AudioBuffer|null
Reusable 60 ms mono buffer of white noise for the "tick".
Type:
- AudioBuffer | null
(inner) out :GainNode|null
Master gain node every voice connects to (→ compressor → speakers).
Type:
- GainNode | null
(inner) reduce :MediaQueryList
Live media query for the visitor's reduced-motion preference.
Type:
- MediaQueryList
(inner) warming :boolean
Whether a deferred setup is already queued (see warm).
Type:
- boolean
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 |
decay |
number | Seconds to fall from |
Returns:
- Type
- void
(inner) muted() → {boolean}
Whether sound should be suppressed right now.
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.
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.
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 |
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. |
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.
Returns:
- Type
- void