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.
data-sound só tocam quando isto está ligado.Instalar
npx shadcn@latest add https://goodui.dev/r/sounds.jsonEle 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 clicarstateful-button: tap, depois success ou error conforme o resultadoswitch: toggle-on / toggle-offcheckbox: pop ao marcar, tick ao desmarcartabs: tick ao selecionarrating: um tick por estrela sob o ponteiro, subindo de tom ao longo da fileira, pop ao escolhernumber-input: tick por passo, mais agudo para +, mais grave para −dialog: open / closetoast-stack (Toaster): success, error, warning ou notify a cada novo toast, de novo quando um toast de carregamento resolveconfetti: celebrate junto com a explosão (fireConfetti({ sound: true }) ou <Confetti sound />)hold-to-confirm: tick quando o toque longo começa, confirm quando terminacopy-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.
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>
)
}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 SSRCrie 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 wayCampos 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 1goodui 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 lightselect, tick, count-tick: selectiontoggle-on / toggle-off: impact light → medium / medium → light, com 60 ms de intervalosuccess, warning, error: notification do mesmo tiponotify: dois impacts light, com 90 ms de intervalosend / receive: light → rigid / soft → lightdelete: impact heavyundo: light → mediumpop: impact rigidwhoosh / slide: três / duas selectionsconfirm: impact medium, depois notification successopen / close: soft → light / light → softdrag-start / drop: impact medium / heavy → lightcelebrate: light, light, medium, depois notification successunlock: rigid, rigid, depois notification successhover: 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