Historically, sounds on the web have gotten a bad rap — and rightly so. They can be jarring, distracting, and sometimes startling to users. However, proper use of sound on an app can provide helpful cues to end users, enriching the user interaction overall.
Sound can be used to highlight specific user actions and accentuate important feedback. When handled elegantly, sound can give life to an otherwise dull user experience. There are many use cases in which sound can enrich user experience. Games and mobile apps may immediately come to mind, but web can also benefit from this enriching user experience.
One golden rule to keep in mind is accessibility, which we will dive into in greater detail later on. A user must have the ability to opt out, and sounds should never auto-play without explicit user consent. With this in mind, the possibilities are endless.
Consider important notifications, new messages in chats when a user has navigated away from the tab or browser, and so on. This is where the useSound
Hook becomes really useful. It helps to seamlessly integrate sound into your React-based UI.
useSound
is a React Hook that allows you to easily add sound to your React projects. It comes with many options for most of the common use cases. It also extends the howler.js library, which enables you to extend the functionality it already provides.
At ~1KB gzipped, and asynchronously loading about 10KB of howler.js, it is small enough that it won’t significantly impact your app’s performance. According to the announcement blog, you get the following functionalities out of the box, and many more:
The package can be installed via either yarn
or npm
:
# yarn yarn add use-sound # npm npm install use-sound
This package exports a single default value: the useSound
Hook.
import useSound from 'use-sound';
This is all you need to start using the Hook. Of course, you will need to import the sound to be used as well. With create-react-app
, you can import this like any other arbitrary file (e.g., an image). You can easily get free sound from resources like Freesound or ZapSplat.
For example:
import ping from '../../sounds/ping.mp3'; const [play, { stop }] = useSound(ping);
As you might have noticed from the imports and usage example above, we destructured play
and stop
from the Hook, which accepts the ping
sound.
These are the two basic methods that can be used for playing and pausing sound. By default, sound doesn’t play until the user interacts with an element or it is intentionally triggered. This is great for accessibility and allows us to lazy-load sound and third-party libraries.
Additionally, the useSound
Hook can accept the path to the sound directly as the first argument. You can also add a config object consisting of the hookOptions
for more control and flexibility — for instance, the playbackRate
, volume
, interrupt
, etc. This is reactive and syncs with the state of the component.
const [volume, setVolume] = React.useState(0.75); const [play] = useSound('/path/to/sound', { volume });
hookOptions
When calling useSound
, you can pass it a variety of options referred to as hookOptions
. The charts below, along with additional details and an exhaustive API list, are available in the useSound
API documentation:
Name | Value |
---|---|
volume |
Number |
playbackRate |
Number |
interrupt |
Boolean |
soundEnabled |
Boolean |
sprite |
spriteMap |
[delegated] |
— |
Besides the play
method, you also have access to the exposedData
object, extending your UI control possibilities:
Name | Value |
---|---|
stop |
Function – (id?: string) => void |
pause |
Function – (id?: string) => void |
isPlaying |
Boolean |
duration |
Number (or null ) |
sound |
Howl (or null ) |
howler.js is an audio library that makes working with audio in JavaScript easy and reliable across all platforms. Any unrecognized option you pass to hookOptions
will be delegated to howler.js. You can see the full list of options in the howler.js docs.
Here’s an example of how we can use onPlayError
to fire a function when there is an error:
const [play] = useSound('/beep.mp3', { onPlayError: () => { console.error('Error occured!'); }, })
Or fire a callback when the sound is muted:
const [play] = useSound('/thong.mp3', { onmute: () => { myCallback() }, })
We will go into use cases with concrete examples of all the core concepts in the next section.
In this section, we will explore some use cases with code samples and recipes. All examples can be explored or edited directly on CodeSandbox.
Two of the more common use case scenarios are popups and notifications. Think something similar to a Facebook notification tab; you want to get the user’s attention when they have a new notification, friend request, message, or a like on their posts.
To simulate this scenario, we will build a simple lookalike navbar with notification icons. We will then have a setInterval
logic that randomly sets notification. I won’t go into the implementation details of the setInterval
, which is available in full in the CodeSandbox. We will instead focus on handling this particular scenario with useSound
.
First, create the AppBar
component. Note that I have also added a checkbox toggle to demonstrate giving the user the ability to permanently turn off or turn on the sound if they so wish. This is important for good user experience and for accessibility.
import React, { useState } from "react"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faBell } from "@fortawesome/free-solid-svg-icons"; import useSound from "use-sound"; import CheckBox from "./CheckBox"; import useInterval from "../hooks/useInterval"; import sound1 from "../assets/sound1.mp3"; const AppBar = () => { const [isRunning, setIsRunning] = useState(true); const [checked, setChecked] = useState(false); const [count, setCount] = useState(0); const [play] = useSound(sound1, { volume: 0.2 }); useInterval( () => { setCount(count + 1); if (checked) { play(); } }, isRunning ? 3000 : null ); const reset = () => { setIsRunning(false); }; const toggle = () => { setChecked(!checked); }; return ( <nav className="appbar"> <div className="toggle"> <CheckBox handleChange={toggle} checked={checked} /> </div> <span className="notification"> <FontAwesomeIcon icon={faBell} onClick={() => reset()} /> {!!count && <span className="badge">{count}</span>} </span> </nav> ); }; export default AppBar;
And the CSS:
.appbar { display: flex; justify-content: space-between; background-color: blue; align-items: center; color: white; height: 50px; } .toggle { margin-left: 5px; } .icons * { margin: 0 5px; }
First, let’s review what we intend to achieve. We want to keep sounding the notification each x seconds until the user checks the notification. This is useful when a user navigates away from the tab or the browser but we would like to keep their attention.
Here we have simply called the play()
method for as long as our condition is true
. To reset or cancel the playing, we simply opt out of playing when isRunning
or notification
is false
.
Another common example is playing, pausing, and then resuming sound. Think Spotify or any other audio streaming app. Let’s quickly build this component (the full code is available in the CodeSandbox).
import React from "react"; import useSound from "use-sound"; const Pause = ({ stop }) => { return ( <svg className="button" viewBox="0 0 60 60" onClick={()=>stop()}> <polygon points="0,0 15,0 15,60 0,60" /> <polygon points="25,0 40,0 40,60 25,60" /> </svg> ); }; const Play = ({ play }) => { return ( <svg className="button" viewBox="0 0 60 60" onClick={play}> <polygon points="0,0 50,30 0,60" /> </svg> ); }; const Player = () => { const [play, { stop, isPlaying }] = useSound(sound3); return ( <div className="player"> {isPlaying ? <Pause stop={stop} /> : <Play play={play} />} </div> ); }; export default Player;
Let’s take a stab at the code above. The Player
component toggles between play
and stop
. Just like with the previous example, we have delegated the play()
and stop()
method, respectively, to handle these cases on click.
The other useful piece of data here is the isPlaying
property. This is a Boolean that tells us whether the sound is currently playing. For this use case, we have employed this property to toggle between play or stop.
Another fun example is increasing pitch or volume.
To demonstrate this, we’d use a simple progress bar. We will increase the length of the progress bar with each click. This example is common in displaying health bars, game status, progress, etc. We will also increase the volume and pitch as the bar grows.
You will notice that the playbackRate
and volume
passed to useSound
are reactive and automatically sync with state. Manipulating any of the exposedData
is as easy as binding it to a state in the component.
import React, { useState } from "react"; import Progress from "react-progressbar"; import useSound from "use-sound"; import sound from "./sound3.mp3"; const ProgressBar = () => { const [status, setStatus] = useState(10); const [playbackRate, setPlaybackRate] = useState(0.75); const [ volume, setVolume]= useState(0.4); const [play] = useSound(sound, { playbackRate, volume }); const handleIncrease = () => { setPlaybackRate(playbackRate => playbackRate + 0.1); setStatus(status => status + 10); setVolume(volume=>volume+1) play(); }; return ( <div> <Progress completed={status} onClick={handleIncrease} /> </div> ); }; export default ProgressBar;
Again, the full code is available on CodeSandbox.
Sprites come in handy when we have to deal with a larger number of sounds in our app. Sprites combine many little sound files into one. This decreases files size, and most importantly, it is better for performance as it avoids many parallel HTTP trips to fetch different sound files.
We will build a simple set of buttons and bind the ID to the sound in the sprite such that each button is responsible for playing different sounds in the sprite.
import React from "react"; import useSound from "use-sound"; import sound from "./sound3.mp3"; function SpriteDemo() { const [play] = useSound(sound, { sprite: { kick: [0, 350], pong: [374, 160], bell: [666, 290], cowbell: [968, 200] } }); const playSound = (e) => { e.preventDefault(); play(e.target.id); }; return ( <> <button id="kick" onClick={e => playSound(e)}> Kick </button> <button id="pong" onClick={e => playSound(e)}> Pong </button> <button id="bell" onClick={e => playSound(e)}> Bell </button> <button id="cowbell" onClick={e => playSound(e)}> Cowbell </button> </> ); }
There are many more options and possibilities — you are only limited by your creativity. The documentation has more recipes for usage.
A number of users would disagree that sound enhances UX on the web. And this isn’t just an auditory preference — it can be a cause of serious annoyance and accessibility issues if not handled properly.
Many visually impaired users rely on screen readers to parse the text on the web into sounds, which is then narrated to them. Stuffing the web with confusing sounds could be jarring for them and produce the opposite effect we had in mind. Hence, it is crucial to think critically about sound on the web. There are a few golden rules to keep in mind to ensure wider usability and accessibility.
It is necessary that all users must opt in to sound — that is, the user can decide whether they want to receive sound at all. Users must have the ability to easily mute or stop the sound, and the must be able to permanently disable sound until they decide otherwise. The control to do this should be readily keyboard accessible, e.g., with Tab key.
More importantly, the web application should be 100 percent usable without sound. For users who are hearing impaired, sound would be all but useless; if there is no other way of meaningfully interacting with the site without sound, that renders the website itself useless. In case of longer audio, attempts should be made to provide alternatives, such as a transcript.
The takeaway is to think about all users and the end goal of using sounds in the first place. For instance, in the notification example above, the user can still see the notifications with or without audio; a badge, color change, count, etc. would make that functionality 100 percent usable without audio.
Audio on the web is under-explored and under-utilized. An elegant, well thought-out use of sound on the web can deeply enrich user experience.
In the above examples, we have barely begun to scratch the surface when it comes to the possibilities. Almost all modern browsers support audio, but the native HTML solution can be hard to configure. The combination of third-party libraries like useSound
and howler.js, along with some creativity, can produce amazing results.
While keeping accessibility in mind, I would implore product designers and developers to experiment and give audio enhancement a second look. Have a resounding time experimenting.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowCompare Prisma and Drizzle ORMs to learn their differences, strengths, and weaknesses for data access and migrations.
It’s easy for devs to default to JavaScript to fix every problem. Let’s use the RoLP to find simpler alternatives with HTML and CSS.
Learn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.