Skip to content

Repository files navigation

tiks

Procedural UI sounds for the web. Zero audio files. Pure synthesis.

npmgzip sizelicense

Every native app has satisfying sounds — iOS toggle clicks, macOS trash crumple, Android keyboard taps. Web apps have nothing. tiks brings that missing sensory layer using the Web Audio API. Every sound is generated at runtime through oscillators, noise buffers, and gain envelopes. No audio files shipped. Just math.

All sounds share a common synthesis engine with a unified theme — so they sound like they belong together.

Install

npm install @rexa-developer/tiks

Quick Start

import{tiks}from'@rexa-developer/tiks'// Initialize (safe to call at any time — the AudioContext is created on the// first real user gesture, so this never triggers an autoplay-policy warning)tiks.init()// Play soundstiks.click()tiks.success()tiks.toggle(true)

That's it. Three lines.

Sounds

MethodCharacterDuration
click()Short, crisp tap~12ms
toggle(true)Rising pitch snap~60ms
toggle(false)Falling pitch snap~60ms
success()Two-note rising chime~200ms
error()Gentle buzz/thud~150ms
warning()Cautious double-tap~180ms
hover()Whisper-soft tick~15ms
pop()Playful bubble pop~80ms
swoosh()Quick transition whoosh~120ms
notify()Bright attention ping~300ms

hover() is a high-frequency primitive — use sparingly. The engine throttles it to one tick per 80 ms by default, so dense pointer movement can't machine-gun the sound. Adjust with init({ hoverThrottleMs }) or pass 0 to disable. Still, reserve hover() for deliberate, low-density targets — never bind it to dense lists, menus, or whole-page hover regions.

Themes

Four built-in themes that change the character of every sound:

tiks.init({theme: 'soft'})// Warm, rounded, Apple-like (default)tiks.init({theme: 'crisp'})// Sharp, tactile, mechanical keyboard feeltiks.init({theme: 'arcade'})// Chunky 8-bit, square-wave chiptune charactertiks.init({theme: 'glass'})// Airy, resonant, high and ringing

Switch at runtime:

tiks.setTheme('crisp')

Custom Themes

import{tiks,defineTheme}from'@rexa-developer/tiks'constbrand=defineTheme({name: 'brand',baseFreq: 500,oscType: 'triangle',decay: 0.8,})tiks.init({theme: brand})

Theme properties:

PropertyTypeDescription
namestringTheme identifier
baseFreqnumberRoot frequency all sounds derive from
noiseColor'white' | 'pink'Noise type for transients
oscTypeOscillatorType'sine''triangle''square''sawtooth'
filterFreqnumberBandpass center for click/transient sounds
filterQnumberFilter resonance
attacknumberAttack time in seconds
decaynumberDecay multiplier (1.0 = normal, 0.5 = snappy)
brightnessnumberHighpass cutoff (higher = brighter)

Controls

tiks.setVolume(0.5)// 0.0 - 1.0 (default: 0.3)tiks.mute()tiks.unmute()tiks.setTheme('crisp')

Options

tiks.init({theme: 'soft',// 'soft' | 'crisp' | 'arcade' | 'glass' | custom themevolume: 0.4,// 0.0 - 1.0muted: false,// Mute/unmute. Last explicit value wins (shared globally).respectReducedMotion: true,// Default: true. Set false to always play. Last explicit value wins across init()/useTiks calls.hoverThrottleMs: 80,// Min ms between hover() sounds. 0 disables.})

React

import{useTiks}from'@rexa-developer/tiks/react'functionToggleButton(){const{ click, toggle }=useTiks()return(<buttononClick={()=>{click()setEnabled(prev=>{toggle(!prev)return!prev})}}>
Toggle
</button>)}

The hook auto-initializes on mount. All returned methods are stable references.

Note:volume and muted are shared globally across all useTiks hooks (they live on the underlying audio engine). theme is per-hook. If two hooks pass different volume values, the last-mounted one wins.

Vue

<script setup lang="ts">import { useTiks } from'@rexa-developer/tiks/vue'const { click, success, error } =useTiks()</script>
<template>
<button@click="click">Click me</button>
</template>

The composable auto-initializes on component mount. Same global/per-instance semantics as the React hook: volume and muted are shared across all useTiks calls, theme is per-composable.

Svelte

import{useTiks}from'@rexa-developer/tiks/svelte'const{ click, success, error }=useTiks()// In your component:// <button on:click={click}>Click me</button>

Call useTiks() inside your component's <script> block. Same global/per-instance semantics: volume and muted are shared globally, theme is per-call. No Svelte APIs are imported — the adapter is a plain function wrapper.

Solid

import{useTiks}from'@rexa-developer/tiks/solid'const{ click, toggle }=useTiks()// In your JSX:// <button onClick={click}>Click me</button>

Call useTiks() at component scope. Same global/per-instance semantics: volume and muted are shared globally, theme is per-call. No Solid APIs are imported — the adapter is a plain function wrapper.

Tree-Shakeable Imports

For minimal bundles, import only the sounds you need:

import{init,click,success}from'@rexa-developer/tiks'init()click()success()

For multiple independent instances (different themes on the same page), use createTiks():

import{createTiks}from'@rexa-developer/tiks'constui=createTiks({theme: 'crisp'})constgame=createTiks({theme: 'arcade'})ui.click()// crisp themegame.notify()// arcade theme

CDN

<scripttype="module">import{tiks}from'https://esm.sh/@rexa-developer/tiks'document.querySelector('#btn').onclick=()=>tiks.click()</script>

Declarative binding (data-tiks)

For a no-JS approach, annotate elements with data-tiks and call bindTiks() once:

<buttondata-tiks="click">Save</button><inputtype="checkbox" data-tiks="toggle"><scripttype="module">import{bindTiks}from'https://esm.sh/@rexa-developer/tiks'bindTiks()</script>

Supported values: click, success, error, warning, hover, pop, swoosh, notify, toggle, toggle-on, toggle-off. Unknown values are silently ignored.

The toggle value auto-detects state: for <input type="checkbox"> it reads el.checked (which reflects the new state after the click); for other elements it reads aria-pressed="true/false". Caveat: if you use aria-pressed on a custom button, update the attribute before your click handler returns — or use the explicit toggle-on / toggle-off values instead.

bindTiks() accepts an optional root element (defaults to document) and returns an unbind function: const unbind = bindTiks(); /* later */ unbind().

Accessibility

  • prefers-reduced-motion: Respected by default — auto-mutes and reacts to runtime preference changes. Pass respectReducedMotion: false to opt out.
  • Sounds are additive: Never the only way to convey information — always pair with visual feedback
  • Global mute: tiks.mute() for user control
  • Default volume is 30%: Subtle, not intrusive

Browser Support

Works in all browsers that support the Web Audio API — Chrome, Firefox, Safari, Edge, and mobile browsers. The library handles autoplay policy automatically by resuming the AudioContext on first interaction.

How It Works

Each sound is a pure function that creates Web Audio nodes, connects them, schedules playback, and lets the browser garbage-collect them:

OscillatorNode ──┐
├──→ GainNode (envelope) ──→ MasterGain ──→ Limiter ──→ speakers
NoiseBuffer ─────┘
↓
FilterNode ───────────────────────────────┘

A transparent limiter sits on the master bus so overlapping sounds during rapid interaction can't hard-clip; it stays inactive at normal levels.

No audio files. No downloads. No decoding. Just oscillators and math.

Size

EntryGzipped
Core (tiks) — 10 sounds, 4 themes, bindTiks, createTiks~3.5KB
React hook (tiks/react)+~380B
Vue composable (tiks/vue)+~280B
Svelte (tiks/svelte) / Solid (tiks/solid)+~90B each

Unused sounds and helpers tree-shake out of bundler builds; the core number is the everything-included CDN cost.

License

MIT

About

Procedural UI sounds for the web. Zero audio files. Pure synthesis.

Resources

Stars

613 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages