Refurbishing 10-year old buttons (in a Web Audio App)

Refurbishing 10-year old buttons (in a Web Audio App)

·

4 min read

Modernizing a 10-year old web audio drum machine under the hood while keeping the historical user interface posed interesting challenges in web development practices.

As part of my 20% project with the Chrome Web Audio team, we're modernizing their rich collection of sample projects. Although we typically revamp both the code and user interface, I chose to preserve the glossy, almost nostalgic, Web 2.0 look of the 10-year old drum machine while completely revamping the underlying HTML, CSS, and JavaScript.

image.png

Taking inventory of the code

buttons.png

The drum machine controls 6 percussion instruments over 16 beats with tri-state buttons: state 0 indicates no note being played, 1 plays a quiet note, and 2 a full note. Let's look at how the original, 10-year old HTML4 code achieved a pixel-perfect layout without CSS:

<!-- The markup below keeps all images in a row on a single long line to avoid 
a bizarre bug where otherwise the browser inserts an extra pixel between
the images. The cost of perfectionism :O{ -->
...
<img id='HiHat_0' class='btn' src='images/button_off.png'><img id='HiHat_1' class='btn' src='images/button_off.png'><img id='HiHat_2' class='btn' src='images/button_off.png'><img id='HiHat_3' class='btn' src='images/button_off.png'><img id='HiHat_4' class='btn' src='images/button_off.png'><img id='HiHat_5' class='btn' src='images/button_off.png'><img id='HiHat_6' class='btn' src='images/button_off.png'><img id='HiHat_7' class='btn' src='images/button_off.png'><img id='HiHat_8' class='btn' src='images/button_off.png'><img id='HiHat_9' class='btn' src='images/button_off.png'><img id='HiHat_10' class='btn' src='images/button_off.png'><img id='HiHat_11' class='btn' src='images/button_off.png'><img id='HiHat_12' class='btn' src='images/button_off.png'><img id='HiHat_13' class='btn' src='images/button_off.png'><img id='HiHat_14' class='btn' src='images/button_off.png'><img id='HiHat_15' class='btn' src='images/button_off.png'>

To cycle the button images, JavaScript sets the src directly:

function drawNote(draw, xindex, yindex) {    
    var elButton = document.getElementById(instruments[yindex] + '_' + xindex);
    switch (draw) {
        case 0: elButton.src = 'images/button_off.png'; break;
        case 1: elButton.src = 'images/button_half.png'; break;
        case 2: elButton.src = 'images/button_on.png'; break;
    }
}

This code is very straightforward and clearly gets the job done! From a modern, web development perspective, there are some issues:

⛔️ img elements as buttons are inaccessible for screenreaders and keyboard usage.
⛔️ Appereance relies on avoiding whitespace in the HTML markup.
⛔️ Controller logic is tightly coupled to appereance: JavaScript sets image source.

Migrating to a Flexbox layout

By changing from inline images to a Flexbox layout, whitespace in the HTML markup is no longer introducing gaps between the buttons.

<div class="buttons_row">
  <img id='HiHat_0' class='btn' src='images/button_off.png'>
  <img id='HiHat_1' class='btn' src='images/button_off.png'>
  ...
  <img id='HiHat_15' class='btn' src='images/button_off.png'>
</div>
.buttons_row {
    align-items: center;
    display: flex;
    height: 42px;
}

✅ Flexbox does not require avoiding whitespace in HTML.

Migrating to semantic input elements

We improve the usability by changing the plain image element to the more expressive image input. By choosing the proper semantic element, browsers can provide better accessiblity for users. Similar to the curb cut effect, this doesn't only help users with screenreaders, but also allows keyboard-based input for everyone.

<input type='image' id='HiHat_0' class='btn' src='images/button_off.png' alt='Toggle Hi-Hat on beat 1'>
<input type='image' id='HiHat_1' class='btn' src='images/button_off.png' alt='Toggle Hi-Hat on beat 2'>
...
<input type='image' id='HiHat_15' class='btn' src='images/button_off.png' alt='Toggle Hi-Hat on beat 16'>

<input type="image"> improves accessibility.
⛔️ srccannot be controlled with CSS, still needs to be set in JavaScript.

Decoupling the appereance from JavaScript

To remove the hardcoded image paths from JavaScript, we change image inputs to plain buttons, which can be fully styled in CSS. To easily query buttons in CSS, we introduce data attribute, data-note, that is set in JavaScript. Finally, to allow more flexible querying from JavaScript, we decompose specially constructed IDs like id="HiHat_0" into two data attributes data-instrument='HiHat' data-rhythm='0'.

<button data-instrument='HiHat' data-rhythm='0' data-note='0' class='btn note' alt='Toggle Hi-Hat on beat 1'></button>
<button data-instrument='HiHat' data-rhythm='1' data-note='0' class='btn note' alt='Toggle Hi-Hat on beat 2'></button>
...          
<button data-instrument='HiHat' data-rhythm='15' data-note='0' class='btn note' alt='Toggle Hi-Hat on beat 16'></button>
function drawNote(note, rhythmIndex, instrument) {    
    const elButton = document.querySelector(`[data-rhythm="${rhythmIndex}"][data-instrument="${instrument}"]`);
    elButton.dataset.note = note;
}
.note {
    background: url(../images/button_off.png) no-repeat center;
    height: 38px;
    width: 38px;
}

.note[data-note="1"] {
    background-image: url(../images/button_half.png);
}

.note[data-note="2"] {
    background-image: url(../images/button_on.png);
}

✅ HTML defines semantic buttons that are fully accessible.
✅ Appereance is fully controlled in CSS.
✅ JavaScript controller is decoupled from visual appearance.

Alternatives considered

I decided against using classes like <button class="hihat rhythtm0 note2" in favor of data attributes, because data attributes seem more expressive and classes are commonly used for pure styling purposes.

I chose to keep the control of state in JavaScript and the HTML lightweight. Alternatively, the tri-state toggle could have been realized in pure HTML and CSS through radio buttons. Either way, JavaScript code needs to override notes when a beat is loaded, so keeping the logic to toggle tri-state notes in JavaScript follows naturally.

Although I typically use TypeScript and frameworks like Angular, React, and Vue.js for web applications, I decided against using a framework because:

  • Plain, "vanilla" JavaScript examples teach you a lot about web standards.
  • The drum machine project has not grown in the last 10 years and is not expected to grow.
  • Omitting the build pipeline makes development accessible for people new to the web.

Conclusion

While keeping the user interface identical, we cleaned up the underlying code of tri-state buttons:

  • HTML markup now defines semantically expressive and accessible button elements.
  • CSS controls the layout with Flexbox and styling with data attributes.
  • JavaScript controls the functional behavior by querying and setting data attributes.

If you're interested in web audio, feel free to explore and contribute to Chrome's open-source Web Audio Samples.