サウンドとハプティクス

再生の瞬間にブラウザで合成される24種類のUIサウンド。音声ファイルなし、ホスティング不要、7 KBのモジュールひとつ。各サウンドは、Cメジャーペンタトニック上の2〜3個のサイン波倍音、柔らかいアタック、フィルターをかけたクリックの小さなレシピで、−14 dBFSでミックスされています。サウンドは訪問者がオンにするまで鳴らず、ブラウザごとに記憶され、ユーザー操作の前には決して鳴りません。モーションとサウンドは別の設定で、prefers-reduced-motionは影響しません。

このブラウザではサウンドがオフです。下の再生ボタンはどちらでも動きます。コンポーネントとdata-soundはオンのときだけ鳴ります。

インストール

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

lib/goodui-sounds.tsに置かれます。sound propを持つコンポーネントは依存関係として宣言しているので、一緒に入ります。モジュールは遅延読み込みで、コンポーネントのsoundがオンのときだけ読み込まれます。使わないアプリには1バイトも含まれません。

すべてのサウンド

カードをクリックすると鳴ります。波形はオフラインでレンダリングしたサウンドで、小さな音も見えるよう正規化しています。

コンポーネント

soundを渡すとコンポーネントのデフォルト音が鳴り、サウンド名を渡せば別の音を選べます。渡さない限りすべてオフです。

  • button: クリックでtap
  • stateful-button: tap、その後は結果に応じてsuccessまたはerror
  • switch: toggle-on / toggle-off
  • checkbox: チェックでpop、解除でtick
  • tabs: 選択でtick
  • rating: ポインター下の星ごとにtick、行に沿って音程が上がり、決定でpop
  • number-input: ステップごとにtick、+で高く、−で低く
  • dialog: open / close
  • toast-stack (Toaster): 新しいトーストごとにsuccess、error、warning、notify。読み込み中トーストの完了時にもう一度
  • confetti: バーストと一緒にcelebrate(fireConfetti({ sound: true }) または <Confetti sound />)
  • hold-to-confirm: 長押し開始でtick、完了でconfirm
  • copy-button: コピー後にpop、失敗でerror
<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>

切り替えはユーザーに

SoundProviderを一度マウントして、設定かヘッダーにトグルを置いてください。訪問者の選択はlocalStorageに保存され、アプリのデフォルトより優先されます。

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

どんな要素でも

プロバイダーをマウントすれば、どんな要素でもコードなしでサウンドを鳴らせます。data-soundはクリック時に(toggleはクリック後のchecked状態を読みます)、data-sound-hoverはポインターが入ったときに鳴ります。

<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

自分で設計する

レシピはボイスのリストです。オシレーターの倍音かフィルター付きノイズで、それぞれに開始時刻、長さ、レベル、任意のグライドがあります。tone()はオクターブ倍音付きの音、noise()はクリックやウーシュを作ります。短く(350 ms未満)、静かに(レベル0.5未満)、スケール上に収めれば、組み込み音と並べても違和感がありません。

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

ボイスのフィールド:wave(sine、triangle、square、sawtooth、noise)、freqとtoはHz、glide、at、dur、attackは秒、gainは0..1、filter / filterTo / qはローパス(オシレーター)またはバンドパス(ノイズ)、detuneはセント。

テスト

renderSound(name)はOfflineAudioContextでレシピをレンダリングしてバッファを返すので、何も鳴らさずに、サウンドが無音でもクリップしてもいないことをテストで検証できます。

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:ハプティクス

同じ名前が、goodui Nativeではハプティクスパターンになります(foundation/haptics.ts)。expo-hapticsのimpact、notification、selectionのタップにタイミングを付けて組み立てています。foundation/soundsのcue(name)は1回の呼び出しでパターンとサウンドを再生するので、一度書いた画面がどのプラットフォームでも自然に感じられます。サウンドはアプリがsetSoundsEnabled(true)を呼ぶまでオフで、iOSとAndroidではアプリがsetSoundPlayerで登録したプレイヤー(expo-audioと専用の短いファイル)を通じて鳴り、Expo webではここと同じように合成されます。

  • tap: impact light
  • select, tick, count-tick: selection
  • toggle-on / toggle-off: impact light → medium / medium → light、60 ms間隔
  • success, warning, error: 同じ種類のnotification
  • notify: light impact 2回、90 ms間隔
  • send / receive: light → rigid / soft → light
  • delete: impact heavy
  • undo: light → medium
  • pop: impact rigid
  • whoosh / slide: selection 3回 / 2回
  • confirm: impact medium、続けてnotification success
  • open / close: soft → light / light → soft
  • drag-start / drop: impact medium / heavy → light
  • celebrate: light、light、medium、続けてnotification success
  • unlock: rigid、rigid、続けてnotification success
  • hover: なし(タッチにホバーはない)
import { cue, setSoundsEnabled } from "@/components/goodui-native/foundation"

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