photo of headphones in the dark
Photo by Jason Leung on Unsplash

Working with Sound in React

Enhancing UI interactions with simple sounds.

5 min read

When added thoughtfully, sound can provide user actions that have more depth and can help improve context. Often times, sound is not even considered when designing websites, but it doesn’t have to be this way!

In this article, you’ll learn how to use sound in React. We’ll start by writing a simple custom React hook using the HTMLAudioElement interface and then take what we wrote and use it in three examples.

This article doesn’t go into detail about when not to use sound. Like most things, you should use your best judgement. Consider whether it’ll be annoying for your users, and if it truly adds to the overall experience. If the answer is not obvious, then you probably shouldn't use sound.

All audio files in this article are from Pixabay.

Writing a Custom React Hook

We'll start things off by diving right away into writing a React hook.

Create a new file named useWithSound.

This custom hook takes a single parameter audioSource, which is a reference to an audio file or path.


import { useRef, useEffect } from 'react';

export const useWithSound = (audioSource) => {
  const soundRef = useRef();

  return {};
}

Next, create a new Audio instance by passing the audioSource to the Audio constructor. This should be inside a useEffect that runs once. We’ll store this instance in a React ref to access it throughout the hook.


export const useWithSound = (audioSource) => {
  const soundRef = useRef();

  useEffect(() => {
    soundRef.current = new Audio(audioSource);
  }, []);

  return {};
}

To play a sound, it’s as easy as calling the .play() method. Pausing is just as simple. We'll write two functions that do this.


export const useWithSound = (audioSource) => {
  ...

  const playSound = () => {
    soundRef.current.play();
  }

  const pauseSound = () => {
    soundRef.current.pause();
  }

  return {};
}

Our hook returns the two functions we just wrote, so any consuming component can use them.


export const useWithSound = (audioSource) => {
  ...

  return {
    playSound,
    pauseSound,
  }
}

And that’s it for the custom hook. Now let’s use it!

Be sure to check out Building an Audio Player With React Hooks for a more advanced use of the HTMLAudioElement API.

The CSS used in these examples is not the focus of this article, so we won’t go into any detail. Please reference the demos for the complete code.

Playing a Sound on Button Click

The first thing we’ll do with our new hook is play a sound when a button is clicked.

Create a new component named ButtonClick and import the useWithSound hook.
This component returns some HTML, including a button with a click handler named handleButtonClick.


import { useWithSound } from './useWithSound';

export const ButtonClick = () => {
  return (
    <section className="example">
      <div className="container">
        <button type="button" onClick={handleButtonClick}>
          Click here
        </button>
      </div>
    </section>
  );
}

Now use the hook by passing it a mouse click sound and have it run within a handleButtonClick function.


import { useWithSound } from './useWithSound';
import click from './assets/click.mp3';

export const ButtonClick = () => {
  const { playSound } = useWithSound(click);

  const handleButtonClick = () => {
    playSound();
  }

  return (...);
}

Playing a Sound When Showing a Message

Next we’ll play a sound when showing a new message on the screen. After dismissing the message, clicking the trigger button will replay the animation and sound.

Just like in the previous example, create a new component and import the hook. We’ll name it Message and the sound we’ll play has the same name.

This time we use a showMessage state value that shows the message when true.


import { useState, useEffect } from 'react';
import { useWithSound } from './useWithSound';
import message from './assets/message.mp3';

export const Message = () => {
  const [showMessage, setShowMessage] = useState(false);

  return ();
}

Next in the return statement, let’s add some HTML.

The markup consists of some containing elements, a button that sets the showMessage state to true, and the message itself.


return (
  <section className="example">
    <div className="container">
      <button 
        type="button"
        className="message-trigger"
        onClick={() => setShowMessage(true)}
      >Show a message</button>
      {showMessage && (
        <div className="message">
          <div>
            <div>You have a new message!</div>
            <a href="#">View</a>
          </div>
          <button type="button" onClick={() => setShowMessage(false)}>X</button>
        </div>
      )}
    </div>
  </section>
);

Now we’ll set up our hook with the sound we imported, and in a separate useEffect hook, play it when the showMessage state value is true.


export const Message = () => {
  const [showMessage, setShowMessage] = useState(false);
  const { playSound } = useWithSound(message);

  useEffect(() => {
    if (showMessage) {
      playSound();
    }
  }, [showMessage]);
	
  return (...);
}

Using Sound in Form Validation

For the last example, we’ll use sound to enhance input validation using two sounds depending on valid or invalid states.

Start by creating a new component named InputValidation.

This component renders an input that prompts the user to type the word “confirm”. If the user types exactly that word, they hear a “success” sound and see a valid state using an icon and green border. Otherwise, if the user types any other sequence of seven characters, they’ll hear an “error” sound and see a message.


import { useState, useEffect } from 'react';
import { useWithSound } from './useWithSound';
import error from './assets/error.mp3';
import success from './assets/success.mp3';
import { ReactComponent as CheckIcon } from './assets/check.svg';

export const InputValidation = () => {
  const [inputVal, setInputVal] = useState('');
  const [isInvalid, setIsInvalid] = useState(false);
  const { playSound: playError } = useWithSound(error);
  const { playSound: playSuccess } = useWithSound(success);

  return ();
}

The component has two state values. inputVal just tracks the input value and isInvalid is a boolean value that determines when we have an invalid state.
At this time you can also import and pass the success and error sounds to the useWithSound hook.

Add constant that combines a check for seven characters and !isInvalid that determines whether we're in a "valid" state. This is used to apply the valid styles and show the icon.


export const InputValidation = () => {
  ...
  const isValid = inputVal.length === 7 && !isInvalid;

  return ();
}
  

Next let’s add the HTML.


export const InputValidation = () => {
  ...

  return (
    <section className="example">
      <div className={`input-wrapper ${isValid && 'valid'} ${isInvalid && 'invalid'}`}>
        <input
          type="text"
          name="confirm"
          value={inputVal}
          onChange={handleChange}
          className="confirm-field"
          placeholder={`Type "confirm"`}
          required
        />
        {isValid && <CheckIcon className="icon" />}

        {isInvalid && (
          <p>The value you entered is invalid</p>
        )}
      </div>
    </section>
  );
}

We have some containing elements just like in the previous examples, a text input, an icon to display when we have a valid state, and a message to show if the input state is invalid. Depending on the state of isValid and isInvalid, we apply some CSS classes for styling purposes.

Now we’ll add a handleChange function to check the length of the value and update the inputVal state if it's seven characters or less.


export const InputValidation = () => {
  ...

  const handleChange = (e) => {
    if (e.target.value.length <= 7) {
      setInputVal(e.target.value);
    }
  }
	
  return (...);
}

We also have a useEffect to determine when the input state is valid, and what to do depending on if it is or isn’t. If the input value matches exactly “confirm”, then we trigger playSuccess. Otherwise, playError is triggered and the isInvalid state is set to true.


export const InputValidation = () => {
  ...

  useEffect(() => {
    if (inputVal.length !== 7) {
      setIsInvalid(false);
      return;
    }

    if (inputVal.toLowerCase() === 'confirm') {
      playSuccess();
    } else {
      setIsInvalid(true);
      playError();
    }
  }, [inputVal]);
	
  return (...);
}

Summary

This article discusses how to work with sound in React. We wrote a custom React hook using the HTMLAudioElement API to play a sound, and then used it in three examples, showing just how easy it is to add basic sounds to your UI!