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.

Sounds sind in diesem Browser aus. Die Abspieltasten unten funktionieren trotzdem; Komponenten und data-sound spielen nur, wenn das an ist.

Installieren

npx shadcn@latest add https://goodui.dev/r/sounds.json

Es 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 Klick
  • stateful-button: tap, dann success oder error je nach Ergebnis
  • switch: toggle-on / toggle-off
  • checkbox: pop beim Anhaken, tick beim Abhaken
  • tabs: tick beim Auswählen
  • rating: ein tick pro Stern unter dem Zeiger, entlang der Reihe höher gestimmt, pop beim Wählen
  • number-input: tick pro Schritt, höher für +, tiefer für −
  • dialog: open / close
  • toast-stack (Toaster): success, error, warning oder notify pro neuem Toast, erneut, wenn ein Lade-Toast auflöst
  • confetti: celebrate mit dem Burst (fireConfetti({ sound: true }) oder <Confetti sound />)
  • hold-to-confirm: tick, wenn das Halten beginnt, confirm, wenn es abgeschlossen ist
  • copy-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.

app/layout.tsx
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>
  )
}
components/sound-toggle.tsx
"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 SSR

Eigene 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 way

Felder 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 1

goodui 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 light
  • select, tick, count-tick: Selection
  • toggle-on / toggle-off: Impact light → medium / medium → light, 60 ms Abstand
  • success, warning, error: Notification desselben Typs
  • notify: zwei leichte Impacts, 90 ms Abstand
  • send / receive: light → rigid / soft → light
  • delete: Impact heavy
  • undo: light → medium
  • pop: Impact rigid
  • whoosh / slide: drei / zwei Selections
  • confirm: Impact medium, dann Notification success
  • open / close: soft → light / light → soft
  • drag-start / drop: Impact medium / heavy → light
  • celebrate: light, light, medium, dann Notification success
  • unlock: rigid, rigid, dann Notification success
  • hover: 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