Codementor Events

Let's write a piano chord visualization application - a vanilla JS beginner-friendly tutorial—part II.

Published May 29, 2022Last updated Apr 15, 2024
Let's write a piano chord visualization application - a vanilla JS beginner-friendly tutorial—part II.

In part I, we learned the basic rules of creating a chord and created our HTML/CSS piano keyboard. In this part, we will finish the HTML/CSS template and make core JavaScript functionality.

Contents:

  1. Minimum music theory explanation for better topic understanding
  2. Setup
  3. [HTML/CSS] Model of a piano keyboard
    3.1 Complete code
  4. [HTML/CSS] Chord root and type selection
    4.1 Complete code
  5. [JS] Chord generation
    5.1 Complete code
  6. [HTML/JS] Putting all together

[HTML/CSS] Chord root and type selection

To complete our HTML/CSS part of the application, we have to add a chord selection form. To avoid scrolling through many note options, I decided to make a selection in the form of a group of buttons for each part.

chord_selector_01.png
Chord selection HTML/CSS result

I separated chord root note selection and type selection since we'll be changing them independently.

NOTE: Chord root note gives the chord a name and establishes the relationship between notes in a chord.

<div class="container-fluid">
  <div class="row">
    <div class="col-md-12">
      <label for="root-notes-group" class="form-label"><i class="bi bi-music-note"></i> Root note</label>
      <div class="btn-toolbar mb-3" role="toolbar" aria-label="Toolbar with button groups">
        <div id="root-notes-group" class="btn-group me-2" role="group" aria-label="Root notes group">
          <button type="button" class="btn btn-outline-light">C</button>
          <button type="button" class="btn btn-outline-light">C#</button>
          <button type="button" class="btn btn-outline-light">D</button>
          <button type="button" class="btn btn-outline-light">D#</button>
          <button type="button" class="btn btn-outline-light">E</button>
          <button type="button" class="btn btn-outline-light">F</button>
          <button type="button" class="btn btn-outline-light">F#</button>
          <button type="button" class="btn btn-outline-light">G</button>
          <button type="button" class="btn btn-outline-light">G#</button>
          <button type="button" class="btn btn-outline-light">A</button>
          <button type="button" class="btn btn-outline-light">A#</button>
          <button type="button" class="btn btn-outline-light">B</button>
        </div>
      </div>
    </div>
  </div>

  <div class="row">
    <div class="col-md-12">
      <label for="chord-types-group" class="form-label"><i class="bi bi-music-note-beamed"></i> Chord type</label>
      <div class="btn-toolbar mb-3" role="toolbar2" aria-label="Toolbar with button groups">
        <div id="chord-types-group" class="btn-group me-2" role="group2" aria-label="Chord types group">
          <button type="button" class="btn btn-outline-light">Major</button>
          <button type="button" class="btn btn-outline-light">Minor</button>
          <button type="button" class="btn btn-outline-light">Diminished</button>
        </div>
      </div>
    </div>
  </div>
</div>

HTML structure is simple. In first group in <div id="root-notes-group" class="btn-group me-2" role="group" aria-label="Root notes group"> each button represents one root note. In second group <div id="chord-types-group" class="btn-group me-2" role="group2" aria-label="Chord types group"> each button represents one chord type.

All of the stylings are provided by the Bootstrap framework. You can find the details of the button group styling with Bootstrap here.

Our application will handle the display of three types of chords: major, minor and diminished.

Now in index.html, replace the part <!-- chord root and type selection --> with chord type selection.

index.html should present the following result.

chord_selector_02.png
Complete HTML/CSS for the chord selection

[HTML/CSS] Chord root and type selection - complete code

In this section, we have only edited index.html file.

index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf8" />
    <title>Piano chord visualizer</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.2/font/bootstrap-icons.css" />
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0-beta1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-0evHe/X+R7YkIZDRvuzKMRqM+OrBnVFBL6DOitfPri4tjfHxaWutUpFmBp4vmVor" crossorigin="anonymous" />
    <link rel="stylesheet" href="piano-keyboard.css" />
  </head>
  <body class="d-flex flex-column h-100 min-vh-100">
    <main class="bg-secondary text-white flex-grow-1">
      <div class="container">
        <div class="row mt-3">
          <div class="col-md-12">
            <h1>Piano chord visualizer</h1>
          </div>
        </div>
        <div class="row mt-5">
          <div class="col-md-6">
            <div class="container-fluid">
              <div class="row">
                <div class="col-md-12">
                  <label for="root-notes-group" class="form-label"><i class="bi bi-music-note"></i> Root note</label>
                  <div class="btn-toolbar mb-3" role="toolbar" aria-label="Toolbar with button groups">
                    <div id="root-notes-group" class="btn-group me-2" role="group" aria-label="Root notes group">
                      <button type="button" class="btn btn-outline-light">C</button>
                      <button type="button" class="btn btn-outline-light">C#</button>
                      <button type="button" class="btn btn-outline-light">D</button>
                      <button type="button" class="btn btn-outline-light">D#</button>
                      <button type="button" class="btn btn-outline-light">E</button>
                      <button type="button" class="btn btn-outline-light">F</button>
                      <button type="button" class="btn btn-outline-light">F#</button>
                      <button type="button" class="btn btn-outline-light">G</button>
                      <button type="button" class="btn btn-outline-light">G#</button>
                      <button type="button" class="btn btn-outline-light">A</button>
                      <button type="button" class="btn btn-outline-light">A#</button>
                      <button type="button" class="btn btn-outline-light">B</button>
                    </div>
                  </div>
                </div>
              </div>

              <div class="row">
                <div class="col-md-12">
                  <label for="chord-types-group" class="form-label"><i class="bi bi-music-note-beamed"></i> Chord type</label>
                  <div class="btn-toolbar mb-3" role="toolbar2" aria-label="Toolbar with button groups">
                    <div id="chord-types-group" class="btn-group me-2" role="group2" aria-label="Chord types group">
                      <button type="button" class="btn btn-outline-light">Major</button>
                      <button type="button" class="btn btn-outline-light">Minor</button>
                      <button type="button" class="btn btn-outline-light">Diminished</button>
                    </div>
                  </div>
                </div>
              </div>
            </div>
          </div>

          <div class="col-md-6 d-flex justify-content-center flex-wrap">
            <div id="piano-keyboard">
              <div class="key key-natural"></div>
              <div class="key key-sharp"></div>
              <div class="key key-natural"></div>
              <div class="key key-sharp"></div>
              <div class="key key-natural"></div>
              <div class="key key-natural"></div>
              <div class="key key-sharp"></div>
              <div class="key key-natural"></div>
              <div class="key key-sharp"></div>
              <div class="key key-natural"></div>
              <div class="key key-sharp"></div>
              <div class="key key-natural"></div>
              <div class="key key-natural"></div>
              <div class="key key-sharp"></div>
              <div class="key key-natural"></div>
              <div class="key key-sharp"></div>
              <div class="key key-natural"></div>
              <div class="key key-natural"></div>
              <div class="key key-sharp"></div>
              <div class="key key-natural"></div>
              <div class="key key-sharp"></div>
              <div class="key key-natural"></div>
              <div class="key key-sharp"></div>
              <div class="key key-natural"></div>
              <div class="key key-natural"></div>
            </div>
          </div>
        </div>
      </div>
    </main>
  </body>
</html>

Section knowledge base
Here you will find links to helpful articles on the topic I don't cover thoroughly in this section.

Button group styling in Bootstrap by getbootstrap.com - Bootstrap button group documentation


[JS] Chord generation

In this section, we start writing the JavaScript part of the application. This section only focuses on generating data we will later need to render changes. Code responsible for selection control and rendering selected options you will find in the next section.

All JS content we put in the index.js file.

// Array storing all existing notes on the piano
const notes = ["c", "c#", "d", "d#", "e", "f", "f#", "g", "g#", "a", "a#", "b"];

We start by defining the constant storing all notes. It will be our entry point for functions generating chord notes.

// Object storing chord types and its notes position
const chordScheme = {
  maj: [0, 4, 7],
  min: [0, 3, 7],
  dim: [0, 3, 6],
};

We store our chord types in an object where object keys are chord types and values are the notes belonging to the chord. The numbers represent the place of the note in the notes array.

Quick reminder, "C major" chord notes distances are 4 and 3 semitones. Note "C" is a first string in the notes array, so its index is 0. Then we increment it by 4 to get the next note—finally, we increment it by 3 to get the last note of the chord. 0, 4 [0 + 4], 7 [0 + 4 + 3]

This way, we can specify a universal scheme for storing our chord types. Then, you can start wondering,

"It works for C-based chords, but what about A, B, G, and other chords?"

We will make it work by shifting the notes array; the way, the desired root note will be under index 0. To do so, we will create a function that:

  • find an index of new root note in the notes array,
  • split notes array into two arrays, where the split point is our found index,
  • return new notes array by combining split arrays with swapped position

js_02.png
Function logic visualtization

For that, we create a function called getNotesFromRoot which accepts rootNote argument.

// Function altering the notes array to change the root note
const getNotesFromRoot = (rootNote) => {
};

First, we find an index of new root note by using the indexOf function. notes.indexOf(rootNote.toLowerCase()); returns -1 or integer value with the position of the rootNote.toLowerCase(). We use the toLowerCase() method to make the search case insensitive. This eliminates the situation when, for example, we have passed the "G#" string, which is the correct note name but doesn't exist in the notes array. We store results in the slicePivot constant. If the slicePivot equals -1, we return an empty array.

arrayTypeVar.indexOf(searchedValue) - method returning position of searchedValue in an array on which function was called

stringTypeVar.toLowerCase() - method returning lowercased value of the string variable on which function was called

// Function altering the notes array to change the root note
const getNotesFromRoot = (rootNote) => {
  let slicePivot = notes.indexOf(rootNote.toLowerCase()); // get the position of new root note
  if (slicePivot === -1) { // return empty array if root note doesn't exists
    return [];
  }
};

Next, we split the notes array into two smaller arrays. For that, we use the slice() method. The first subpart we call fromPivot contains notes from the position of the new root note to the end of the array. The second subpart is toPivot and includes notes from the beginning to the first before the new root note. Finally, we combine two subparts in one by returning a new array of copied values of the arrays. For that, we use the array spread operator.

Alternatively, we can use the concat() method. If then, the return statement will be return fromPivot.concat(toPivot);.

Our function returning shifted notes array is now ready.

arrayTypeVar.slice(startIndex, endIndex) - method returning a shallow copy of an array containing elements from range <startIndex, endIndex); Element under the endIndex is not included in the copy. Ex.: [1, 2, 3, 4, 5, 6].slice(2, 5) will return [3, 4, 5];

arrayTypeVar.concat(array1, array2, ..., arrayN) - method returning a new array by concatenating array with the arrays provided in arguments. Ex.: [1, 2, 3].concat([4, 5, 6]) will return [1, 2, 3, 4, 5, 6];

// Function altering the notes array to change the root note
const getNotesFromRoot = (rootNote) => {
  let slicePivot = notes.indexOf(rootNote.toLowerCase()); // get the position of new root note
  if (slicePivot === -1) { // return empty array if root note doesn't exists
    return [];
  }

  const fromPivot = notes.slice(slicePivot, notes.length); // get part of array fom new root note to the end of array
  const toPivot = notes.slice(0, slicePivot); // get part of array fom beginning to new root note

  return [...fromPivot, ...toPivot]; // return new array combining fromPivot and toPivot together
};

Finally, we'll create a function returning chord notes. Let's call that function getChord and make it accept arguments rootNote and chordType. The logic of function is following:

  • We prepare an array with the notes starting from the chords root note - for that, we use the previously created getNotesFromRoot.
  • We check if the notesFromRoot is not empty.
  • We check if the chordScheme[chordType] returned a value. Valid properties of chordScheme object are "maj", "min" and "dim" which corresponds to "major", "minor" and "diminished."
    We return mapped chordScheme[chordType] if the checks have passed. We return a new array when replacing the notes indexes with notes themselves.
    If checks have failed, we return an empty array.

arrayTypeVar.map((element) => {/* ... */}) - method returning a new array with modified elements as instructed in the passed mapping function as an argument. Ex.: [1, 2, 3].map((element) => {return element + 5;}) will return [6, 7, 8]. Each element will be transformed by mapping function.

// Function returning chord notes
const getChord = (rootNote, chordType) => {
  const notesFromRoot = getNotesFromRoot(rootNote); // get array of notes, beginning from the rootNote value

  if (notesFromRoot.length > 0 && chordScheme[chordType]) {
    return chordScheme[chordType].map((noteIndex) => notesFromRoot[noteIndex]); // map selected chord type array into array of note names 
  }

  return [];
};

[JS] Chord generation - complete code

In this section, we have only edited index.js file.

index.js

// Array storing all existing notes on the piano
const notes = ["c", "c#", "d", "d#", "e", "f", "f#", "g", "g#", "a", "a#", "b"];

// Object storing chord types and its notes position
const chordScheme = {
  maj: [0, 4, 7],
  min: [0, 3, 7],
  dim: [0, 3, 6],
};

// Function altering the notes array to change the root note
const getNotesFromRoot = (rootNote) => {
  let slicePivot = notes.indexOf(rootNote.toLowerCase()); // get the position of new root note
  if (slicePivot === -1) { // return empty array if root note doesn't exists
    return [];
  }

  const fromPivot = notes.slice(slicePivot, notes.length); // get part of array fom new root note to the end of array
  const toPivot = notes.slice(0, slicePivot); // get part of array fom beginning to new root note

  return [...fromPivot, ...toPivot]; // return new array combining fromPivot and toPivot together
};

// Function returning chord notes
const getChord = (rootNote, chordType) => {
  const notesFromRoot = getNotesFromRoot(rootNote); // get array of notes, beginning from the rootNote value

  if (notesFromRoot.length > 0 && chordScheme[chordType]) {
    return chordScheme[chordType].map((noteIndex) => notesFromRoot[noteIndex]); // map selected chord type array into array of note names 
  }

  return [];
};

We can test what we have coded so far by running the index.html file in the browser. Then, in the browser's developer tools console, we can directly call the function getChord.

js_03.png
Execution of getChord() function in the Chrome's Developer Tool console

Section knowledge base
Here you will find links to helpful articles on the topic I don't cover thoroughly in this section.

Array.prototype.slice() by developer.mozilla.org - Array slice() method
Spread syntax by developer.mozilla.org - Spread operator
Array.prototype.map() by developer.mozilla.org - Array map() method


In part III, we will add interactivity to our HTML template to mark the piano keys depending on the selected chord.

Discover and read more posts from Andrzej Gorgoń
get started
post commentsBe the first to share your opinion
Emanuel Covelli
21 days ago

hello is part 3 ready ?

Andrzej Gorgoń
20 days ago

Just published :) Enjoy!

justin stile
5 months ago

I joined this site to like this post. Thank you.

Andrzej Gorgoń
5 months ago

Thank you! Due to some circumstances, I didn’t finished part three, which I will this week. Thanks for bringing me motivation :)

Andrzej Gorgoń
17 days ago

In the end, it took waaaay to long., but it’s here https://www.codementor.io/@andrzejgorgonag/let-s-write-a-piano-chord-visualization-application-a-vanilla-js-beginner-friendly-tutorial-part-iii-1zt3hq20s0. I promise, next posts will be like 1 part and more regular. And question, if music related stuff is interesting :)

Paul Canales
8 months ago

Thank you for the tutorial. Doesn’t index.js have to have a link in the index.html or is that in part iii?

Andrzej Gorgoń
5 months ago

Part three will come this week. Soooooorry for huge delay. But yes, js file has to be included in an html file in order to work.

Andrzej Gorgoń
17 days ago
Show more replies