Sons et haptique
24 sons d’interface synthétisés dans le navigateur à l’instant où ils jouent : pas de fichier audio, rien à héberger, un module de 7 Ko. Chaque son est une petite recette de deux ou trois partiels sinusoïdaux sur une pentatonique de do majeur, une attaque douce et un clic filtré, mixés à −14 dBFS. Les sons restent coupés tant que le visiteur ne les active pas, le choix est mémorisé par navigateur, et rien ne joue avant un geste de l’utilisateur. Mouvement et son sont deux préférences distinctes : prefers-reduced-motion ne les modifie pas.
data-sound ne jouent que lorsque c’est activé.Installation
npx shadcn@latest add https://goodui.dev/r/sounds.jsonIl atterrit dans lib/goodui-sounds.ts. Les composants avec une prop sound le déclarent en dépendance, il vient donc avec eux. Le module est chargé en lazy et seulement quand un composant a sound activé : une app qui ne l’utilise jamais n’en embarque rien.
Tous les sons
Cliquez sur une carte pour l’entendre. La forme d’onde est le son rendu hors ligne, normalisé pour que les plus discrets restent lisibles.
Composants
Passez sound pour jouer le son par défaut du composant, ou un nom de son pour en choisir un autre. Tout est coupé tant que vous ne le passez pas.
button: tap au clicstateful-button: tap, puis success ou error selon le résultatswitch: toggle-on / toggle-offcheckbox: pop quand coché, tick quand décochétabs: tick à la sélectionrating: un tick par étoile sous le pointeur, de plus en plus aigu le long de la rangée, pop au choixnumber-input: tick par pas, plus aigu pour +, plus grave pour −dialog: open / closetoast-stack (Toaster): success, error, warning ou notify par nouveau toast, à nouveau quand un toast de chargement se résoutconfetti: celebrate avec l’explosion (fireConfetti({ sound: true }) ou <Confetti sound />)hold-to-confirm: tick au début du maintien, confirm quand il se terminecopy-button: pop après une copie, error en cas d’échec
<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>Laissez le choix aux gens
Montez SoundProvider une fois et placez un interrupteur dans vos réglages ou votre header. Le choix du visiteur est enregistré dans localStorage et l’emporte sur le réglage par défaut de l’app.
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>
)
}N’importe quel élément
Une fois le provider monté, n’importe quel élément peut jouer un son sans code. data-sound joue au clic (toggle lit l’état coché après le clic), data-sound-hover à l’entrée du pointeur.
<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 SSRCréez le vôtre
Une recette est une liste de voix : partiels d’oscillateur ou bruit filtré, chacun avec un début, une durée, un niveau et un glissando optionnel. tone() donne une note avec son partiel à l’octave, noise() un clic ou un whoosh. Gardez-le court (moins de 350 ms), discret (niveaux sous 0,5) et dans la gamme, et il s’intégrera aux sons intégrés.
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 wayChamps d’une voix : wave (sine, triangle, square, sawtooth ou noise), freq et to en Hz, glide, at, dur et attack en secondes, gain de 0 à 1, filter / filterTo / q pour le passe-bas (oscillateurs) ou le passe-bande (bruit), detune en cents.
Tests
renderSound(name) rend une recette via un OfflineAudioContext et renvoie le buffer : un test peut vérifier qu’un son n’est ni silencieux ni saturé sans rien jouer.
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 : haptique
Les mêmes noms sont des motifs haptiques dans goodui Native (foundation/haptics.ts), construits à partir des taps impact, notification et selection d’expo-haptics avec leur timing. cue(name) de foundation/sounds joue le motif et le son en un seul appel : un écran écrit une fois sonne juste sur chaque plateforme. Les sons restent coupés tant que l’app n’appelle pas setSoundsEnabled(true) ; sur iOS et Android ils passent par un lecteur que l’app enregistre avec setSoundPlayer (expo-audio avec ses propres fichiers courts), sur Expo web ils sont synthétisés comme ici.
tap: impact lightselect, tick, count-tick: selectiontoggle-on / toggle-off: impact light → medium / medium → light, à 60 ms d’écartsuccess, warning, error: notification du même typenotify: deux impacts light, à 90 ms d’écartsend / receive: light → rigid / soft → lightdelete: impact heavyundo: light → mediumpop: impact rigidwhoosh / slide: trois / deux selectionsconfirm: impact medium, puis notification successopen / close: soft → light / light → softdrag-start / drop: impact medium / heavy → lightcelebrate: light, light, medium, puis notification successunlock: rigid, rigid, puis notification successhover: rien (pas de hover au tactile)
import { cue, setSoundsEnabled } from "@/components/goodui-native/foundation"
setSoundsEnabled(settings.sounds) // from your settings screen
cue("confirm") // haptic pattern + sound, one vocabulary