Skip to main content
Enhance your notification system with context-appropriate sounds for success, error, warning, and info messages.

Basic Notification

Play a sound when showing a toast notification:
import { useSound } from "@/hooks/use-sound"
import { notificationPop } from "@/sounds/notification-pop"
import { toast } from "sonner"
import { Button } from "@/components/ui/button"

export function BasicNotification() {
  const [playNotification] = useSound(notificationPop)

  const showNotification = () => {
    playNotification()
    toast("New message received!")
  }

  return (
    <Button onClick={showNotification}>
      Show Notification
    </Button>
  )
}

Success Notifications

1

Install the sound

npx soundcn@latest add success-chime
2

Import and use

import { useSound } from "@/hooks/use-sound"
import { successChime } from "@/sounds/success-chime"
import { toast } from "sonner"
import { Button } from "@/components/ui/button"

export function SuccessToast() {
  const [playSuccess] = useSound(successChime)

  const handleSuccess = () => {
    playSuccess()
    toast.success("Task completed successfully!")
  }

  return (
    <Button onClick={handleSuccess}>
      Complete Task
    </Button>
  )
}

Error Notifications

Use a distinct sound for errors to grab attention:
import { useSound } from "@/hooks/use-sound"
import { errorBuzz } from "@/sounds/error-buzz"
import { toast } from "sonner"
import { Button } from "@/components/ui/button"

export function ErrorToast() {
  const [playError] = useSound(errorBuzz)

  const handleError = () => {
    playError()
    toast.error("Failed to save changes")
  }

  return (
    <Button variant="destructive" onClick={handleError}>
      Trigger Error
    </Button>
  )
}

Complete Toast System

Create a unified toast system with different sounds:
import { useSound } from "@/hooks/use-sound"
import { successChime } from "@/sounds/success-chime"
import { errorBuzz } from "@/sounds/error-buzz"
import { notificationPop } from "@/sounds/notification-pop"
import { toast } from "sonner"

export function useToastSound() {
  const [playSuccess] = useSound(successChime)
  const [playError] = useSound(errorBuzz)
  const [playInfo] = useSound(notificationPop, { volume: 0.6 })

  return {
    success: (message: string) => {
      playSuccess()
      toast.success(message)
    },
    error: (message: string) => {
      playError()
      toast.error(message)
    },
    info: (message: string) => {
      playInfo()
      toast(message)
    },
  }
}

Form Validation Feedback

Provide immediate audio feedback for form validation:
import { useState } from "react"
import { useSound } from "@/hooks/use-sound"
import { successChime } from "@/sounds/success-chime"
import { errorBuzz } from "@/sounds/error-buzz"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"

export function FormWithSounds() {
  const [email, setEmail] = useState("")
  const [playSuccess] = useSound(successChime)
  const [playError] = useSound(errorBuzz)

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault()
    
    if (email.includes("@")) {
      playSuccess()
      alert("Form submitted!")
    } else {
      playError()
      alert("Invalid email")
    }
  }

  return (
    <form onSubmit={handleSubmit} className="space-y-4">
      <Input 
        type="email"
        placeholder="Enter your email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
      />
      <Button type="submit">Submit</Button>
    </form>
  )
}

Real-time Notifications

Play sounds for real-time events like incoming messages:
import { useEffect } from "react"
import { useSound } from "@/hooks/use-sound"
import { notificationPop } from "@/sounds/notification-pop"
import { toast } from "sonner"

export function RealtimeNotifications() {
  const [playNotification] = useSound(notificationPop, { volume: 0.7 })

  useEffect(() => {
    // Simulate WebSocket connection
    const ws = new WebSocket("wss://example.com/notifications")

    ws.onmessage = (event) => {
      const notification = JSON.parse(event.data)
      playNotification()
      toast(notification.message)
    }

    return () => ws.close()
  }, [])

  return (
    <div>
      <p>Listening for notifications...</p>
    </div>
  )
}
For real-time notifications, consider using a lower volume (0.5-0.7) to avoid startling users, especially if notifications are frequent.

Priority Levels

Adjust volume and playback rate based on notification priority:
import { useSound } from "@/hooks/use-sound"
import { notificationPop } from "@/sounds/notification-pop"
import { errorBuzz } from "@/sounds/error-buzz"
import { toast } from "sonner"

type Priority = "low" | "medium" | "high" | "urgent"

export function usePriorityNotification() {
  const [playPop] = useSound(notificationPop)
  const [playUrgent] = useSound(errorBuzz)

  const notify = (message: string, priority: Priority = "medium") => {
    switch (priority) {
      case "low":
        playPop({ volume: 0.3 })
        toast(message)
        break
      case "medium":
        playPop({ volume: 0.6 })
        toast(message)
        break
      case "high":
        playPop({ volume: 0.9 })
        toast(message, { duration: 5000 })
        break
      case "urgent":
        playUrgent({ volume: 1.0 })
        toast.error(message, { duration: 10000 })
        break
    }
  }

  return { notify }
}

Muting Controls

Let users control notification sounds:
import { createContext, useContext, useState, ReactNode } from "react"
import { useSound } from "@/hooks/use-sound"
import { notificationPop } from "@/sounds/notification-pop"
import { successChime } from "@/sounds/success-chime"
import { errorBuzz } from "@/sounds/error-buzz"
import { toast } from "sonner"

type SoundContextType = {
  soundEnabled: boolean
  toggleSound: () => void
  playNotification: () => void
  playSuccess: () => void
  playError: () => void
}

const SoundContext = createContext<SoundContextType | undefined>(undefined)

export function SoundProvider({ children }: { children: ReactNode }) {
  const [soundEnabled, setSoundEnabled] = useState(true)
  
  const [playPop] = useSound(notificationPop, { soundEnabled })
  const [playChime] = useSound(successChime, { soundEnabled })
  const [playBuzz] = useSound(errorBuzz, { soundEnabled })

  return (
    <SoundContext.Provider value={{
      soundEnabled,
      toggleSound: () => setSoundEnabled(!soundEnabled),
      playNotification: playPop,
      playSuccess: playChime,
      playError: playBuzz,
    }}>
      {children}
    </SoundContext.Provider>
  )
}

export function useSoundContext() {
  const context = useContext(SoundContext)
  if (!context) throw new Error("useSoundContext must be used within SoundProvider")
  return context
}

Chaining Notifications

Create a sequence of sounds for multi-step processes:
import { useSound } from "@/hooks/use-sound"
import { notificationPop } from "@/sounds/notification-pop"
import { successChime } from "@/sounds/success-chime"
import { toast } from "sonner"
import { Button } from "@/components/ui/button"

export function ProcessWithSounds() {
  const [playProgress] = useSound(notificationPop)
  const [playSuccess] = useSound(successChime)

  const startProcess = async () => {
    toast("Starting upload...")
    playProgress()

    await new Promise(resolve => setTimeout(resolve, 1000))
    
    toast("Processing...")
    playProgress({ playbackRate: 1.2 })

    await new Promise(resolve => setTimeout(resolve, 1000))
    
    toast.success("Upload complete!")
    playSuccess()
  }

  return (
    <Button onClick={startProcess}>
      Upload File
    </Button>
  )
}

Troubleshooting

Add debouncing to prevent sound spam:
import { useCallback, useRef } from "react"
import { useSound } from "@/hooks/use-sound"
import { notificationPop } from "@/sounds/notification-pop"

export function DebouncedNotification() {
  const [playSound] = useSound(notificationPop)
  const lastPlayedRef = useRef(0)
  const DEBOUNCE_MS = 1000

  const playDebounced = useCallback(() => {
    const now = Date.now()
    if (now - lastPlayedRef.current > DEBOUNCE_MS) {
      playSound()
      lastPlayedRef.current = now
    }
  }, [playSound])

  return playDebounced
}
Play the sound slightly before or with the toast:
// Play sound first (recommended)
playSound()
toast("Message")

// Or use onPlay callback
const [play] = useSound(sound, {
  onPlay: () => toast("Message")
})
You can conditionally use different sounds:
import { useTheme } from "next-themes"
import { click8bit } from "@/sounds/click-8bit"
import { clickSoft } from "@/sounds/click-soft"

const { theme } = useTheme()
const sound = theme === "dark" ? click8bit : clickSoft
const [play] = useSound(sound)

Available Notification Sounds

  • notification-pop - Friendly, short pop (539ms)
  • success-chime - Positive confirmation (490ms)
  • error-buzz - Harsh error indicator (104ms)
Install notification sounds:
npx soundcn@latest add notification-pop success-chime error-buzz

Build docs developers (and LLMs) love