Sonidos y hápticos

24 sonidos de interfaz sintetizados en el navegador en el momento en que suenan: sin archivos de audio, nada que alojar, un módulo de 7 KB. Cada sonido es una pequeña receta de dos o tres parciales senoidales sobre una pentatónica de do mayor, un ataque suave y un clic filtrado, mezclados a −14 dBFS. Los sonidos están apagados hasta que el visitante los activa, se recuerdan por navegador y nunca suenan antes de un gesto del usuario. Movimiento y sonido son preferencias separadas: prefers-reduced-motion no los cambia.

Los sonidos están desactivados en este navegador. Los botones de reproducción de abajo funcionan igualmente; los componentes y data-sound solo suenan cuando esto está activado.

Instalación

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

Aterriza en lib/goodui-sounds.ts. Los componentes con una prop sound lo listan como dependencia, así que viene con ellos. El módulo se carga de forma perezosa y solo cuando un componente tiene sound activado, así que una app que nunca lo usa no incluye nada de él.

Todos los sonidos

Haz clic en una tarjeta para oírlo. La forma de onda es el sonido renderizado sin conexión, normalizado para que los más silenciosos se sigan leyendo.

Componentes

Pasa sound para reproducir el sonido predeterminado del componente, o el nombre de un sonido para elegir otro. Todo está apagado a menos que lo pases.

  • button: tap al hacer clic
  • stateful-button: tap y luego success o error según el resultado
  • switch: toggle-on / toggle-off
  • checkbox: pop al marcar, tick al desmarcar
  • tabs: tick al seleccionar
  • rating: un tick por estrella bajo el puntero, cada vez más agudo a lo largo de la fila, pop al elegir
  • number-input: tick por paso, más agudo para +, más grave para −
  • dialog: open / close
  • toast-stack (Toaster): success, error, warning o notify por cada toast nuevo, y de nuevo cuando un toast de carga se resuelve
  • confetti: celebrate con la explosión (fireConfetti({ sound: true }) o <Confetti sound />)
  • hold-to-confirm: tick cuando empieza la pulsación, confirm cuando se completa
  • copy-button: pop tras copiar, error cuando falla
<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>

Dale el interruptor a la gente

Monta SoundProvider una vez y pon un interruptor en tus ajustes o en la cabecera. La elección del visitante se guarda en localStorage y gana al valor predeterminado de la app.

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>
  )
}

Cualquier elemento

Con el provider montado, cualquier elemento puede reproducir un sonido sin código. data-sound suena al hacer clic (toggle lee el estado marcado después del clic), data-sound-hover cuando entra el puntero.

<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

Diseña el tuyo

Una receta es una lista de voces: parciales de oscilador o ruido filtrado, cada una con un inicio, una duración, un nivel y un glide opcional. tone() da una nota con su parcial de octava, noise() un clic o un whoosh. Mantenlo corto (menos de 350 ms), silencioso (niveles por debajo de 0.5) y dentro de la escala, y encajará junto a los integrados.

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

Campos de una voz: wave (sine, triangle, square, sawtooth o noise), freq y to en Hz, glide, at, dur y attack en segundos, gain 0..1, filter / filterTo / q para el paso bajo (osciladores) o el paso banda (ruido), detune en cents.

Pruebas

renderSound(name) renderiza una receta a través de un OfflineAudioContext y devuelve el buffer, así que un test puede comprobar que un sonido no es silencioso ni satura sin reproducir nada.

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: hápticos

Los mismos nombres son patrones hápticos en goodui Native (foundation/haptics.ts), construidos con los toques de impacto, notificación y selección de expo-haptics con su temporización. cue(name) de foundation/sounds reproduce el patrón y el sonido en una sola llamada, así que una pantalla escrita una vez se siente bien en todas las plataformas. Los sonidos están apagados hasta que la app llama a setSoundsEnabled(true); en iOS y Android suenan a través de un reproductor que la app registra con setSoundPlayer (expo-audio con sus propios archivos cortos), y en Expo web se sintetizan como aquí.

  • tap: impacto ligero
  • select, tick, count-tick: selección
  • toggle-on / toggle-off: impacto ligero → medio / medio → ligero, con 60 ms de separación
  • success, warning, error: notificación del mismo tipo
  • notify: dos impactos ligeros, con 90 ms de separación
  • send / receive: ligero → rígido / suave → ligero
  • delete: impacto fuerte
  • undo: ligero → medio
  • pop: impacto rígido
  • whoosh / slide: tres / dos selecciones
  • confirm: impacto medio y luego notificación de éxito
  • open / close: suave → ligero / ligero → suave
  • drag-start / drop: impacto medio / fuerte → ligero
  • celebrate: ligero, ligero, medio y luego notificación de éxito
  • unlock: rígido, rígido y luego notificación de éxito
  • hover: nada (no hay hover en pantallas táctiles)
import { cue, setSoundsEnabled } from "@/components/goodui-native/foundation"

setSoundsEnabled(settings.sounds)        // from your settings screen
cue("confirm")                           // haptic pattern + sound, one vocabulary