StormBiz Tech Blog

Website Design, Design Courses and Tech related articles.
9 minutes reading time (1769 words)

How to Use JavaScript to Play Sound (Typewriter Effect)

Today, we’re going to create a typewriter effect using CSS and JavaScript. It will happen when a user hovers over a postcard image. And to push things further, we’ll use JavaScript to play a typing sound on the page and sync it with the letters as they appear.

Note: we’ll assume that this effect is for desktop screens only. Covering its functionality on mobile/touch screens is beyond the scope of this exercise. At the end of the tutorial, I’ll propose two ways for optimizing it for responsive scenarios.

Our JavaScript Typewriter Effect

Check out the final demo! Click the sound toggle to turn audio on, and hover over each postcard to see the typewriter effect in action.

1. Begin by Downloading the Page Assets

For this exercise, we’re going to need some assets.

So first, I’ve grabbed four images from Unsplash. Next, a sound icon from iconmonstr. Then, an old typewriter typing sound effect from Mixkit. Finally, a custom typewriter font (web font) from Envato Elements. Merchant Ledger - Font Pack

As a Pro member on CodePen, I’ll upload these files to the Asset Manager.

Need More Typewriter Fonts?

Envato Elements has a huge collection of typewriter and type fonts to help make this project your own. Subscribe today and get unlimited downloads!

2. Continue With the Page Markup

The page markup will consist of the following elements:

A div element that will contain a title and a button. A button class will determine if a sound will play each time we hover over an image. By default, the sound won’t play. To enable it, we have to click the button. A div element that will contain four images along with their captions. By default, all captions will be invisible. Each one of them will appear as soon as we hover over the associated image. Lastly, ideally, they have to be short. An audio element that will be responsible for embedding the sound on the page. Although optional, we’ll give it preload="auto" to inform browsers that it should load on page load.

Here’s the page markup:  

<div class="grid grid-btn"> <h1>...</h1> <button type="button" class="btn-sound btn-sound-off" aria-label="Enable sound"> <svg width="24" height="24" aria-hidden="true"> <path d="M15 23l-9.309-6h-5.691v-10h5.691l9.309-6v22zm-9-15.009v8.018l8 5.157v-18.332l-8 5.157zm14.228-4.219c2.327 1.989 3.772 4.942 3.772 8.229 0 3.288-1.445 6.241-3.77 8.229l-.708-.708c2.136-1.791 3.478-4.501 3.478-7.522s-1.342-5.731-3.478-7.522l.706-.706zm-2.929 2.929c1.521 1.257 2.476 3.167 2.476 5.299 0 2.132-.955 4.042-2.476 5.299l-.706-.706c1.331-1.063 2.182-2.729 2.182-4.591 0-1.863-.851-3.529-2.184-4.593l.708-.708zm-12.299 1.299h-4v8h4v-8z" /> </svg> </button> </div> <div class="grid grid-images"> <figure> <img src="/IMG_SRC" alt="IMG_ALT" width="IMG_WIDTH" height="IMG_HEIGHT"> <figcaption class="animate">...</figcaption> </figure> <!-- more figure elements here --> </div> <audio preload="auto"> <source src="/AUDIO_SRC" type="audio/wav"> </audio>

3. Define the Styles

Coming up next, we’ll discuss the most important aspects of our styles. For the sake of simplicity, as our main focus will be on JavaScript, we won’t cover the reset styles here. So, straight to the point:

We’ll use CSS Grid to create the layout for the two wrapper divs. We’ll use the ::before pseudo-element of the toggle (sound) button to indicate that the sound is off. Initially, this will appear. The image captions will be absolutely positioned elements and invisible by default. Depending on the image, they will appear either on its top or bottom. All characters of each caption will appear sequentially on the image hover. This will be achieved thanks to the transition-delay property that we’ll add to them through JavaScript. However, they should simultaneously disappear when we stop hovering over an image. We’ll add 4px spacing to the empty spans (characters) of each caption. So far our markup doesn’t contain spans. But soon enough, we’ll generate them through JavaScript.

Here are our main styles:

/*CUSTOM VARIABLES HERE*/ .grid { display: grid; max-width: 1200px; margin: 0 auto; } .grid-btn { grid-template-columns: auto auto; grid-gap: 20px; align-items: center; justify-content: center; margin-bottom: 30px; } .grid-images { grid-template-columns: 1fr 1fr; grid-gap: 20px 70px; } .grid-btn .btn-sound { position: relative; display: flex; padding: 5px; border: 2px solid var(--white); } .grid-btn .btn-sound-off::before { content: ""; position: absolute; top: 50%; left: 0; width: 100%; border-top: 2px solid var(--white); transform: translateY(-50%) rotate(45deg); } .grid-btn .btn-sound svg { fill: var(--white); } .grid-images figure { position: relative; transform: rotate(5deg); transform-origin: bottom left; border: 10px solid var(--white); cursor: pointer; backface-visibility: hidden; } .grid-images figure:nth-child(even) { transform: rotate(-5deg); transform-origin: bottom right; } .grid-images img { display: block; } .grid-images .animate { position: absolute; top: 30px; left: 10px; right: 10px; display: flex; flex-wrap: wrap; justify-content: center; padding: 0 10px; overflow: hidden; text-align: center; mix-blend-mode: difference; } .grid-images figure:last-child .animate { top: auto; bottom: 30px; } .grid-images .animate span { font-size: clamp(18px, 2.5vw, 40px); opacity: 0; transition: all 0.01s ease-in-out; } .grid-images .animate span:empty { margin: 0 4px; } .grid-images figure:hover .animate span { opacity: 1; } .grid-images figure:not(:hover) .animate span[style] { transition-delay: 0s !important; }

4. Add Interactivity

We’ll start by looping through all figures and for each one of them, several actions will take place:

const figures = document.querySelectorAll(".grid-images figure"); for (const figure of figures) { generateCharactersMarkup(figure); figure.addEventListener("mouseenter", mouseenterHandler); figure.addEventListener("mouseleave", mouseleaveHandler); }

Note: even safer, you might want to run these actions on page load (listen for the load event).

Modify the Captions’ Markup 

First, we’ll call the generateCharactersMarkup() function and pass to it the related figure element.

Here’s the function declaration:

function generateCharactersMarkup(el) { let index = 0; const textBlock = el.querySelector(".animate"); const characters = textBlock.textContent.split(""); const charactersHTML = characters .map(function (character) { let markup = ""; if (character == " ") { markup = "<span></span>"; if (index == 0) index = 1; } else { let style = ""; if (index != 0) { const sec = 0.15 * index; const secRounded = Math.round(sec * 100) / 100; style = `style="transition-delay:${secRounded}s"`; } markup = `<span ${style}>${character}</span>`; index++; } return markup; }) .join(" "); textBlock.innerHTML = charactersHTML; }

Inside this function:

We’ll find the image caption and loop through its characters. We’ll wrap each character around a span element. We’ll check to see if the character is the first one or equal to space.  If this isn’t the case, it will receive the transition-delay property. The value of this property will specify the amount of time to wait before showing the target character. In our example, each transition will occur after 150ms of the start of its previous one. In your projects, you can adjust this number via the sec variable.

So, if we start with this markup:

<figcaption class="animate">Welcome to Iceland!</figcaption>

After executing the function, it will look as follows:

Notice that the first character and the spaces don’t receive an inline style.

Of course, instead of programmatically generating this markup, we could have added it by default on our HTML. But this will have two disadvantages. Firstly, it will bloat the HTML. Secondly, it will make its maintenance difficult in case any change is required.

Play Sound with JavaScript

At this point let’s put the sound in the loop.

Remember that it will be off by default.

Its state will be handled by the btn-sound-off CSS class and sound JavaScript flag.

Each time we click the button, both these will be updated. For instance, if the sound is on, the button won’t contain this class and the value of this flag will be true.

Here’s the related JavaScript code:

const btnSound = document.querySelector(".grid-btn .btn-sound"); let sound = false; btnSound.addEventListener("click", function () { sound = !sound; const ariaLabel = this.getAttribute("aria-label") == "Enable sound" ? "Disable sound" : "Enable sound"; this.setAttribute("aria-label", ariaLabel); this.classList.toggle("btn-sound-off"); });

When the sound is on, it should play on image hover. But, for how long? That’s the thing we need to consider. Well, our demo audio file is pretty long, about 24 seconds. Obviously, we don’t want to play it all. Just a small part of it until the transition effect finishes.

To do so, we first have to calculate the duration of this effect. Taking that into account, we’ll then initialize a timer that will play the sound for this amount of time.

To better understand it, let’s go through the effect of the first image. If you check your browser console, you’ll notice that its last character has a 2.4 seconds delay. That means, the total effect will last 2.4 seconds (we don’t count the transition duration as it is almost zero). To keep the synchronization between the transition effect and audio playtime, we’ll play the first 2.4 seconds of the audio and then pause it.

For this implementation, we’ll use the mouseenter event. Here’s the declaration of its callback:

... let timer; function mouseenterHandler() { if (sound) { const spans = this.querySelectorAll(".animate span[style]"); const duration = spans.length * 0.15 * 1000; /*SECOND METHOD FOR CALCULATING THE AUDIO DURATION BASED ON THE EFFECT DURATION*/ /*const style = window.getComputedStyle( this.querySelector(".animate span[style]:last-child") ); const duration = style.getPropertyValue("transition-delay").split("s")[0] * 1000;*/ clearTimeout(timer);; timer = setTimeout(function () { audio.pause(); }, duration); } }

Consider in the code above the different ways we can use to calculate the audio duration.

On the other hand, the sound should pause and reset back to its initial position on mouse out.

For this implementation, we’ll use the mouseleave event. Here’s the declaration of its callback:

... function mouseleaveHandler() { if (sound) { audio.pause(); audio.currentTime = 0; } }


Phew, that’s all folks! Thank you for joining me on this long journey! We covered a lot of things today. Not only did we build a cool typing effect on hover, but also made it more realistic by synchronizing it with an old typewriter sound.

Here’s a reminder of what we built:

Touch and Mobile Devices

As we discussed in the introduction, this effect isn’t optimized for mobile/touch devices. Here are two possible solutions that you can try:

Disable the effect on these interfaces. For example, write some code for touch screen detection, and in that case, show the captions by default. Oppositely, if you want to keep the effect for all devices, be sure to capture touch events.

Of course, depending on your needs, you can decouple the effect from the mouse events and keep it standalone or as a part of other events like the scroll one.

As the last thing, keep in mind that this effect might not work under all circumstances without extra customization. For example, as mentioned previously, the image captions should not be lengthy and split into multiple lines like this.

As always, thanks a lot for reading!

More JavaScript Tutorials on Tuts+


An Introduction to JavaScript Event Listeners for Web Designers

Anna Monus


Essential Cheat Sheet: Convert jQuery to JavaScript

Anna Monus


How to Create Smooth Page Transitions with JavaScript

Adi Purdila


Build a Simple Weather App With Vanilla JavaScript

George Martsoukos


How to Build a Grayscale to Color Effect on Scroll (CSS & JavaScript)

George Martsoukos


How to Implement Smooth Scrolling With Vanilla JavaScript

George Martsoukos

Original author: George Martsoukos
How To Survive
Are You Pulling Your Hair Out?

By accepting you will be accessing a service provided by a third-party external to


Wait a minute, while we are rendering the calendar

Latest Blogs

Reward Credit

Login/Register for credits