Sounds und Haptik
24 UI-Sounds, im Browser synthetisiert in dem Moment, in dem sie spielen: keine Audiodateien, nichts zu hosten, ein 7-KB-Modul. Jeder Sound ist ein kleines Rezept aus zwei oder drei Sinus-Teiltönen auf einer C-Dur-Pentatonik, einem weichen Einschwingen und einem gefilterten Klick, gemischt bei −14 dBFS. Sounds sind aus, bis der Besucher sie einschaltet, werden pro Browser gemerkt und spielen nie vor einer Nutzergeste. Bewegung und Sound sind getrennte Einstellungen: prefers-reduced-motion ändert sie nicht.
data-sound spielen nur, wenn das an ist.Installieren
npx shadcn@latest add https://goodui.dev/r/sounds.jsonEs landet in lib/goodui-sounds.ts. Komponenten mit einem sound-Prop führen es als Abhängigkeit, es kommt also mit ihnen mit. Das Modul wird lazy geladen und nur, wenn eine Komponente sound aktiv hat; eine App, die es nie nutzt, liefert nichts davon aus.
Jeder Sound
Klick auf eine Karte, um ihn zu hören. Die Wellenform ist der offline gerenderte Sound, normalisiert, damit leise lesbar bleiben.
Komponenten
Übergib sound, um den Standard der Komponente zu spielen, oder einen Sound-Namen für einen anderen. Alles ist aus, solange du es nicht übergibst.
button: tap beim Klickstateful-button: tap, dann success oder error je nach Ergebnisswitch: toggle-on / toggle-offcheckbox: pop beim Anhaken, tick beim Abhakentabs: tick beim Auswählenrating: ein tick pro Stern unter dem Zeiger, entlang der Reihe höher gestimmt, pop beim Wählennumber-input: tick pro Schritt, höher für +, tiefer für −dialog: open / closetoast-stack (Toaster): success, error, warning oder notify pro neuem Toast, erneut, wenn ein Lade-Toast auflöstconfetti: celebrate mit dem Burst (fireConfetti({ sound: true }) oder <Confetti sound />)hold-to-confirm: tick, wenn das Halten beginnt, confirm, wenn es abgeschlossen istcopy-button: pop nach dem Kopieren, error, wenn es fehlschlägt
<Button sound onClick={save}>Save</Button>
<Switch sound label="SMS reminders" />
<Rating sound defaultValue={4} />
<Dialog sound title="Cancel this booking?" onConfirm={cancel} trigger={<Button variant="outline">Cancel</Button>} />
<Toaster sound /> {/* success, error, warning or notify per toast */}
<HoldToConfirm sound="unlock" onConfirm={deleteAccount}>Delete account</HoldToConfirm>Gib den Leuten den Schalter
Mounte SoundProvider einmal und leg einen Umschalter in deine Einstellungen oder den Header. Die Wahl des Besuchers wird in localStorage gespeichert und gewinnt über den App-Standard.
import { SoundProvider } from "@/lib/goodui-sounds"
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<SoundProvider>{children}</SoundProvider> {/* enabled: app default, persist, volume, attributes */}
</body>
</html>
)
}"use client"
import { useSound } from "@/lib/goodui-sounds"
export function SoundToggle() {
const { enabled, toggle, play } = useSound()
return (
<button type="button" role="switch" aria-checked={enabled} onClick={() => { toggle(); play(enabled ? "toggle-off" : "toggle-on", { force: true }) }}>
Sounds {enabled ? "on" : "off"}
</button>
)
}Jedes Element
Mit gemountetem Provider kann jedes Element ohne Code einen Sound spielen. data-sound spielt beim Klick (toggle liest nach dem Klick den checked-Zustand), data-sound-hover, wenn der Zeiger hineinfährt.
<button data-sound="tap">Book</button>
<input type="checkbox" data-sound="toggle" />
<a href="/pricing" data-sound-hover>Pricing</a> {/* "hover", very subtle */}API
import { play, useSound, setSoundEnabled, isSoundEnabled, setSoundVolume, SOUND_NAMES, type SoundName } from "@/lib/goodui-sounds"
play("success") // false when sounds are off, before the first gesture, or without Web Audio
play("tick", { rate: 1.1, volume: 0.6 }) // rate: pitch and speed, volume: 0..1 on top of the -14 dBFS bus
play("notify", { force: true }) // previews and settings: ignores the toggle
setSoundEnabled(true) // saved in localStorage (unless <SoundProvider persist={false} />)
setSoundVolume(0.5)
const { enabled, toggle, setEnabled, play } = useSound() // reads "off" during SSREigene entwerfen
Ein Rezept ist eine Liste von Stimmen: Oszillator-Teiltöne oder gefiltertes Rauschen, jede mit Start, Länge, Pegel und optionalem Glide. tone() ergibt einen Ton mit seinem Oktav-Teilton, noise() einen Klick oder ein Whoosh. Halt es kurz (unter 350 ms), leise (Pegel unter 0,5) und auf der Skala, dann fügt es sich neben die eingebauten ein.
import { defineSound, tone, noise, play } from "@/lib/goodui-sounds"
// A cash-register "cha-ching": a click, then two notes a fifth apart with a bright octave.
defineSound("cha-ching", [
noise(0.02, { gain: 0.2, filter: 3000 }),
...tone(1046.5, 0.12, { at: 0.03, gain: 0.4, shine: 0.3 }), // C6
...tone(1568, 0.26, { at: 0.11, gain: 0.42, shine: 0.3 }), // G6
])
play("cha-ching") // or <button data-sound="cha-ching">
defineSound("tap", [...]) // built-ins can be replaced the same wayFelder einer Stimme: wave (sine, triangle, square, sawtooth oder noise), freq und to in Hz, glide, at, dur und attack in Sekunden, gain 0..1, filter / filterTo / q für den Lowpass (Oszillatoren) oder Bandpass (Rauschen), detune in Cent.
Testen
renderSound(name) rendert ein Rezept durch einen OfflineAudioContext und gibt den Buffer zurück; ein Test kann also prüfen, dass ein Sound weder stumm ist noch übersteuert, ohne etwas abzuspielen.
const buffer = await renderSound("success")
const data = buffer.getChannelData(0)
const peak = Math.max(...data.map(Math.abs)) // the built-ins stay under 0.3 at volume 1goodui Native: Haptik
Dieselben Namen sind in goodui Native haptische Muster (foundation/haptics.ts), gebaut aus den Impact-, Notification- und Selection-Taps von expo-haptics mit Timing. cue(name) aus foundation/sounds spielt Muster und Sound in einem Aufruf, ein einmal geschriebener Screen fühlt sich also auf jeder Plattform richtig an. Sounds sind aus, bis die App setSoundsEnabled(true) aufruft; auf iOS und Android spielen sie über einen Player, den die App mit setSoundPlayer registriert (expo-audio mit eigenen kurzen Dateien), auf Expo Web werden sie wie hier synthetisiert.
tap: Impact lightselect, tick, count-tick: Selectiontoggle-on / toggle-off: Impact light → medium / medium → light, 60 ms Abstandsuccess, warning, error: Notification desselben Typsnotify: zwei leichte Impacts, 90 ms Abstandsend / receive: light → rigid / soft → lightdelete: Impact heavyundo: light → mediumpop: Impact rigidwhoosh / slide: drei / zwei Selectionsconfirm: Impact medium, dann Notification successopen / close: soft → light / light → softdrag-start / drop: Impact medium / heavy → lightcelebrate: light, light, medium, dann Notification successunlock: rigid, rigid, dann Notification successhover: nichts (kein Hover auf Touch)
import { cue, setSoundsEnabled } from "@/components/goodui-native/foundation"
setSoundsEnabled(settings.sounds) // from your settings screen
cue("confirm") // haptic pattern + sound, one vocabulary