Sons e haptics

24 sons de interface sintetizados no navegador no instante em que tocam: sem arquivos de áudio, nada para hospedar, um módulo de 7 KB. Cada som é uma pequena receita de dois ou três parciais senoidais em uma pentatônica de dó maior, um ataque suave e um clique filtrado, mixados a −14 dBFS. Os sons ficam desligados até o visitante ligá-los, a escolha é lembrada por navegador, e nunca tocam antes de um gesto do usuário. Movimento e som são preferências separadas: prefers-reduced-motion não muda nada aqui.

Os sons estão desligados neste navegador. Os botões de reprodução abaixo funcionam de qualquer forma; componentes e data-sound só tocam quando isto está ligado.

Instalar

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

Ele vai para lib/goodui-sounds.ts. Componentes com a prop sound o listam como dependência, então ele vem junto com eles. O módulo é carregado sob demanda e só quando um componente tem sound ativo, então um app que nunca o usa não entrega nada dele.

Todos os sons

Clique em um card para ouvir. A forma de onda é o som renderizado offline, normalizado para que os mais baixos continuem legíveis.

Componentes

Passe sound para tocar o som padrão do componente, ou o nome de um som para escolher outro. Tudo fica desligado a menos que você passe a prop.

  • button: tap ao clicar
  • stateful-button: tap, depois success ou error conforme o resultado
  • switch: toggle-on / toggle-off
  • checkbox: pop ao marcar, tick ao desmarcar
  • tabs: tick ao selecionar
  • rating: um tick por estrela sob o ponteiro, subindo de tom ao longo da fileira, pop ao escolher
  • number-input: tick por passo, mais agudo para +, mais grave para −
  • dialog: open / close
  • toast-stack (Toaster): success, error, warning ou notify a cada novo toast, de novo quando um toast de carregamento resolve
  • confetti: celebrate junto com a explosão (fireConfetti({ sound: true }) ou <Confetti sound />)
  • hold-to-confirm: tick quando o toque longo começa, confirm quando termina
  • copy-button: pop depois de copiar, error quando falha
<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>

Dê o controle às pessoas

Monte SoundProvider uma vez e coloque um toggle nas suas configurações ou no header. A escolha do visitante é salva em localStorage e vence o padrão do 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>
  )
}

Qualquer elemento

Com o provider montado, qualquer elemento pode tocar um som sem código. data-sound toca ao clicar (toggle lê o estado marcado depois do clique), data-sound-hover quando o ponteiro entra.

<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

Crie o seu

Uma receita é uma lista de vozes: parciais de oscilador ou ruído filtrado, cada uma com início, duração, nível e um glide opcional. tone() dá uma nota com seu parcial de oitava, noise() um clique ou um whoosh. Mantenha curto (menos de 350 ms), baixo (níveis abaixo de 0.5) e na escala, e ele vai combinar com os sons embutidos.

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 uma voz: wave (sine, triangle, square, sawtooth ou noise), freq e to em Hz, glide, at, dur e attack em segundos, gain 0..1, filter / filterTo / q para o passa-baixa (osciladores) ou passa-banda (ruído), detune em cents.

Testes

renderSound(name) renderiza uma receita por um OfflineAudioContext e retorna o buffer, então um teste pode verificar que um som não está mudo nem clipando sem tocar 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: haptics

Os mesmos nomes são padrões hápticos no goodui Native (foundation/haptics.ts), construídos com os toques impact, notification e selection do expo-haptics, com temporização. cue(name) de foundation/sounds toca o padrão e o som em uma única chamada, então uma tela escrita uma vez fica certa em todas as plataformas. Os sons ficam desligados até o app chamar setSoundsEnabled(true); no iOS e no Android eles tocam por um player que o app registra com setSoundPlayer (expo-audio com arquivos curtos próprios), no Expo web são sintetizados como aqui.

  • tap: impact light
  • select, tick, count-tick: selection
  • toggle-on / toggle-off: impact light → medium / medium → light, com 60 ms de intervalo
  • success, warning, error: notification do mesmo tipo
  • notify: dois impacts light, com 90 ms de intervalo
  • send / receive: light → rigid / soft → light
  • delete: impact heavy
  • undo: light → medium
  • pop: impact rigid
  • whoosh / slide: três / duas selections
  • confirm: impact medium, depois notification success
  • open / close: soft → light / light → soft
  • drag-start / drop: impact medium / heavy → light
  • celebrate: light, light, medium, depois notification success
  • unlock: rigid, rigid, depois notification success
  • hover: nada (não há hover no toque)
import { cue, setSoundsEnabled } from "@/components/goodui-native/foundation"

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