Sound
Synthesized clicks for every control, and how to add them.
Buttons, switches, menus and sliders can click like real hardware. Sound is optional and ships as its own registry item, so an app that installs only components stays silent.
npx shadcn@latest add https://www.ui95.dev/r/sound.jsonThe install puts three things in your project: the sound player, a sounds.json file next to your components.json, and a sync script at scripts/dither-sounds.mjs.
Render the player once, at the end of your root layout. Every UI95 component on every page uses that one player, so you never add sound code to a component.
import { Ui95Sounds } from "@/components/ui/ui95-sounds"
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
{children}
<Ui95Sounds />
</body>
</html>
)
}Then run the sync script. It copies in the sounds for the components you have installed, and nothing else. Run it again whenever you add a component. It is better to add this step to your AGENTS.md file. It only adds what is missing, so your edits to sounds.json stay put.
node scripts/dither-sounds.mjs syncThere are no audio files to download. Each sound is a short recipe in sounds.json, and the browser builds it into sound the first time it plays. A recipe stacks a few layers, such as a burst of noise for the tap and a short tone for the click. Each layer fades in and out within a few thousandths of a second.
{
"button.press": {
"note": "Tactile switch: bandpassed noise bump plus a square click",
"layers": [
{
"wave": "noise",
"attack": 0.0003,
"decay": 0.006,
"gain": 0.6,
"filter": { "type": "bandpass", "frequency": 3500, "q": 2 }
},
{
"wave": "square",
"frequency": 74.4,
"frequencyEnd": 375,
"attack": 0,
"decay": 0.00158,
"gain": 0.07
}
],
"jitter": { "cents": 100, "gainDb": 6, "decay": 0.12 }
}
}A real switch never sounds exactly the same twice, so neither does this one. The jitter setting nudges the pitch, volume and length a little on every press, which keeps a run of fast clicks from sounding like a machine gun.
The player listens once for the whole page. When someone presses a control, it reads the control's data-slot, which every UI95 component already has, and plays the matching sound: a press and a release for a button, an on or an off for a switch, a tick for each step of a slider.
Browsers block sound until a visitor interacts with the page. The page stays silent until the first click, and every click after that makes sound.
To change how something sounds, edit its recipe in sounds.json. Raise a layer's gain to make it louder, or lengthen its decay to let it ring longer.
Set a sound to false to switch it off, and use volume for the overall level. Set "enabled": false to start your app with all sound off. Visitors who turn sound on or off keep their choice, even when it differs from your default.
{
"enabled": true,
"volume": 0.6,
"sounds": {
"tab.select": false
}
}To change a single element, give it a data-sound attribute. Use none to silence it, or the name of another sound to borrow that one.
<Button data-sound="none">Quiet</Button>
<Button data-sound="keycap">Clacky</Button>