Play Sound on :hover

Avatar of Chris Coyier
Chris Coyier on

When you google around for how to play a sound with CSS, you’ll find:

  1. Some stuff about Counter Strike: Source
  2. Some stuff about play-during and cue-before and stuff that looks promising but really it’s for aural stylesheets (accessibility / screen reader stuff) not just how to make donkey grunts when you roll over a menu item in any ol’ browser

I’d argue that sounds are part of design and thus the ability to play/trigger it belongs in CSS, but alas, we’re not there yet. To play sounds when the mouse goes over a certain area, we’re going to need to rely on HTML5 or Flash. But nobody around here wants to deal with Flash right? So let’s do it with HTML5, which can play sound through its <audio> element (Firefox 3.5+, Chrome 3+, Opera 10.5+, Safari 4+, IE 9+). To get as much browser support as we can, we’ll do it like this with both an MP3 source (WebKit and IE) and an OGG source (Firefox and Opera).

<audio>
	<source src="audio/beep.mp3"></source>
	<source src="audio/beep.ogg"></source>
	Your browser isn't invited for super fun audio time.
</audio>

If you insert the code exactly as above into a page, you won’t see or hear anything. If you want a little player element, make sure to use the controls attribute (<audio controls>). If you want it to play but not be seen, make sure to use the autoplay element (<audio autoplay>). Or both…

Our goal is to have the sound play when the mouse hovers over a certain element, like a menu item. Again unfortunately, we can’t tell an <audio> element what to do through CSS, so we’ll need JavaScript. To play the sound with JavaScript:

var audio = document.getElementsByTagName("audio")[0];
audio.play();

// or with an ID

var audio = document.getElementById("mySoundClip");
audio.play();

Let’s use jQuery, just because it’s going to make selecting and dealing with events easier.

var audio = $("#mySoundClip")[0];
audio.play();

So to make this sound begin to play when the mouse hovers over a certain element:

var audio = $("#mySoundClip")[0];
$("nav a").mouseenter(function() {
  audio.play();
});

Another way…

The teaser page for the Goodfoot mobile app uses a similar technique to play weird groaning noises (via Dave Rupert) when you hover over the yeti dude. They do it by injecting a new audio element into the DOM everytime that yeti dude is hovered:

$("#speak").mouseenter(function(){
	$("<audio></audio>").attr({ 
		'src':'audio/'+Math.ceil(Math.random() * 5)+'.mp3', 
		'volume':0.4,
		'autoplay':'autoplay'
	}).appendTo("body");
});

That could probably be improved a little, to support OGG as well. Not sure what the volume attribute is all about, that’s not spec nor supported anywhere I’ve seen. This is a modified from Jeffrey Way:

function addSource(elem, path) {
  $('<source>').attr('src', path).appendTo(elem);
}

$("#speak").mouseenter(function(){
     var audio = $('<audio />', {
       autoPlay : 'autoplay'
     });
     addSource(audio, 'audio/'+Math.ceil(Math.random() * 5)+'.mp3');
     addSource(audio, 'audio/'+Math.ceil(Math.random() * 5)+'.ogg');
     audio.appendTo('body');     
});

I’m totally fine with this approach, since it seems to work fine and that’s the bottom line. After the sound has been played once it seems to cache and play quickly, which is good.

Another way to handle it would be to put all three audio elements onto the page right away.

<audio preload="auto" id="sound-1" > ... src & fallback ... </audio>
<audio preload="auto" id="sound-2" > ... src & fallback ... </audio>
<audio preload="auto" id="sound-3" > ... src & fallback ... </audio>

Then randomly choose one to play:

$("#speak").mouseenter(function() {
    $("#sound-" + Math.ceil(Math.random() * 3))[0].play();
});

Trials and Troubles: Overlapping Sounds

My original idea for playing with this was a navigation menu that played a little clicking sound while hovering over them. Immediately this uncovered a problem: you can hover over menu items triggering a .play() a lot faster than that sound can finish playing. A single audio element can’t play it’s own sound in an overlapping way. It just ignores further requests to .play() until it’s completed.

My first try was to use .pause() to stop the playing and then .play() again, but in my testing that doesn’t help much. It seems to honor the pause but then not the play in many cases.

The smoothest way I could find to do it was to duplicate the audio element for each menu item. That way each menu item has it’s own audio clip to play and the sounds can overlap

$("nav a") // loop each menu item
  .each(function(i) {
    if (i != 0) { // only clone if more than one needed
      $("#beep")
        .clone()
        .attr("id", "beep-" + i)
        .appendTo($(this).parent()); 
    }
    $(this).data("beeper", i); // save reference 
  })
  .mouseenter(function() {
    $("#beep-" + $(this).data("beeper"))[0].play();
  });
$("#beep").attr("id", "beep-0"); // get first one into naming convention

View Demo   Download Files

Related