Typing Sound Input
Text input with a synthetic mechanical click on each keystroke: square oscillator 300–500 Hz via Web Audio API, CSS progress bar — zero dependency, zero canvas.
You'll get access to the interactive demo with a free account.
This effect is part of Effect.Labs — 811 vanilla effects, some free, some premium. Atmosphere has 57 effects, including 10 free. Explore the category →
3 usage examples



How it works
On each keydown event, a square OscillatorNode is instantiated through the Web Audio API. The frequency is drawn randomly between 300 and 500 Hz (300 + Math.random() * 200) to vary the timbre slightly between consecutive keystrokes, mimicking the variability of a real keyboard. The total sound duration is 50 ms.
The volume follows an exponential envelope: the GainNode starts at 0.02, then exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.05) brings it near-silence in 50 ms. This profile — instant attack, fast decay — produces the percussive click of a mechanical key without lingering resonance. The oscillator is stopped and released automatically at t+50 ms (stop()), with no memory leak.
The AudioContext is created as a lazy singleton (getSndCtx()) with a webkitAudioContext fallback for Safari ≤ 14. It stays suspended until the first keydown — compliant with the user gesture requirement enforced by Chrome 71+ and Safari 12.1+.
The progress bar (.snd-typing-bar) is a <div> whose width is recalculated in pure CSS on each keystroke: Math.min(100, (input.value.length + 1) × 5) + '%'. It reaches 100% at 20 characters. The CSS property transition: width .1s smooths the animation without RAF or extra JS.
Accessibility
- prefers-reduced-motion: absent from the code. The CSS bar (
transition: width .1s) is not suppressed when the OS enables reduced motion — add@media (prefers-reduced-motion: reduce) { .snd-typing-bar { transition: none } }to comply with WCAG 2.3.3. - Sound is not triggered automatically: the AudioContext only starts after the first keypress. No unexpected noise on page load — compliant with WCAG 1.4.2 (audio control).
- The
<input>is natively keyboard-focusable via Tab. However, no<label>is associated with it in the provided code — only theplaceholderacts as a label, which is a screen-reader gap (WCAG 1.3.1). Add a<label for="sndTypingInput">oraria-labelin integration. - The
.snd-typing-indicatorbar carries norole="progressbar",aria-valuenow, oraria-valuemax— progress is not communicated to screen readers. Add these attributes if the metric carries semantic value for the user. - Text contrast: the input on
#0a0a0fbackground with#fffcolor reaches ≥ 15:1 (WCAG AA and AAA). The placeholder at ~70% white opacity sits around 10:1 — still AA-compliant.
Browser compatibility
Requires Web Audio API — available in all modern browsers since 2014. Zero external dependency, zero canvas.
If both <code>AudioContext</code> and <code>webkitAudioContext</code> are absent (IE 11, Opera Mini), <code>playTone</code> throws silently (the oscillator creation block is never reached). The text input and progress bar remain fully functional — only the sound is missing.
The code
HTML structure to paste into your page (CSS + JS available with a premium account):
<div class="snd-typing-container">
<input
type="text"
class="snd-typing-input"
id="sndTypingInput"
placeholder="Tapez quelque chose..."
>
<div class="snd-typing-indicator">
<div class="snd-typing-bar" id="sndTypingBar"></div>
</div>
</div>
Full HTML + CSS + JS, copy-paste ready — with hundreds of premium effects.
Customize
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
| Frequency range | 300–500 Hz | In the keydown listener, edit 300 + Math.random() * 200. Example: 200 + Math.random() * 100 gives a deeper sound (vintage typewriter); 500 + Math.random() * 300 gives a higher pitch (chiclet keyboard). |
| Oscillator wave type | 'square' | 3rd argument of playTone(freq, 0.05, 'square', 0.02). 'sine' = soft and round, 'triangle' = neutral, 'sawtooth' = more electronic and bright. 'square' is closest to a mechanical relay timbre. |
| Sound duration | 0.05 s (50 ms) | 2nd argument of playTone. 0.05 = dry key click, 0.12 = light piano note, 0.20 = perceptible organ timbre. Drop below 0.03 for a nearly imperceptible tick. |
| Volume (initial gain) | 0.02 | 4th argument of playTone. 0.02 = subtle (ambient), 0.05 = present, 0.10 = loud. Stay under 0.08 to avoid startling users with headphones. |
| Bar color | #6366f1 | CSS background property of .snd-typing-bar. Replace with your brand primary. Gradients are supported: linear-gradient(90deg, #6366f1, #ec4899). |
| Bar fill rate | × 5 (100% at 20 chars) | In Math.min(100, (input.value.length + 1) * 5), adjust the multiplier: * 3 → 100% at 33 chars (long email), * 10 → 100% at 10 chars (PIN code). |
| CSS transition speed | 0.1 s | The transition: width .1s property on .snd-typing-bar. Use .3s ease-out for smoother fill, or none for instant display (and prefers-reduced-motion compliance). |
FAQ
AudioContext until an explicit user gesture (click or keypress). The first keydown creates and resumes the context — the sound is absent only on that single keystroke. This is browser policy, not a bug. To work around it, call getSndCtx().resume() in a click handler on the page (e.g. a 'Enable sound' button) before the user starts typing.document.getElementById('sndTypingInput') at execution time — the DOM must be mounted. In React, wrap the initialization in useEffect(() => { /* run script */ }, []) or use a ref directly. In Vue, call it in the mounted() hook. For multiple instances, generate unique IDs per component (e.g. sndTypingInput-${uuid}) and add the event listener manually.AudioBufferSourceNode loaded via AudioContext.decodeAudioData(). Load the WAV/MP3 once at mount, store the AudioBuffer, and on each keystroke create a new AudioBufferSourceNode, connect it to the GainNode, and call start(). The listener structure stays identical — only the audio source changes.