Building an Interactive Blues Scale Visualizer with React and TypeScript

Building an Interactive Blues Scale Visualizer with React and TypeScript

March 9, 2025

8 min read

Building an Interactive Blues Scale Visualizer with React and TypeScript
Watch on YouTube

In this post, we'll explore how to create an interactive visualization of the blues scale on a guitar fretboard using React and TypeScript. You can explore the complete source code in the GitHub repository and try out the live demo here. The project is built on RadzionKit, a robust boilerplate that provides essential components and utilities for a streamlined development experience.

A Minor Blues Scale on the Guitar
A Minor Blues Scale on the Guitar

Core Data Structures

Let's start by defining our core data structure. A musical scale in our application is represented by a TypeScript interface with three essential properties: type, tonality and rootNote, which defines the starting note of the scale.

import { Tonality } from "../tonality"
import { ScaleType } from "./ScaleType"

export type Scale = {
  type: ScaleType
  tonality: Tonality
  rootNote: number
}

Scale Types and Patterns

The scale's tonality determines its fundamental character, being either "minor" or "major".

export const tonalities = ["minor", "major"] as const
export type Tonality = (typeof tonalities)[number]

Each scale comes in three types: full, pentatonic, and blues. The scale pattern represents the sequence of intervals (measured in semitones) between consecutive notes. For example, in a minor blues scale, we move up 3 semitones, then 2, then 1, and so on, creating its distinctive bluesy sound.

import { Tonality } from "../tonality"
import { ScalePattern } from "./ScalePattern"

export const scaleTypes = ["full", "pentatonic", "blues"] as const
export type ScaleType = (typeof scaleTypes)[number]

export const scalePatterns: Record<
  ScaleType,
  Record<Tonality, ScalePattern>
> = {
  full: {
    major: [2, 2, 1, 2, 2, 2, 1],
    minor: [2, 1, 2, 2, 1, 2, 2],
  },
  pentatonic: {
    major: [2, 2, 3, 2, 3],
    minor: [3, 2, 2, 3, 2],
  },
  blues: {
    major: [2, 1, 1, 3, 2, 3],
    minor: [3, 2, 1, 1, 3, 2],
  },
}

export const scalePatternsNumber = 5

State Management

We implement URL-based state management to handle scale changes, encoding the scale parameters directly in the URL (e.g., /scale/A/blues/minor)

import { getValueProviderSetup } from "@lib/ui/state/getValueProviderSetup"
import { useRouter } from "next/router"
import { useCallback } from "react"
import { toUriNote } from "@product/core/note/uriNote"
import { Scale } from "@product/core/scale/Scale"

export const makeScalePath = ({ type, tonality, rootNote }: Scale) =>
  `/scale/${toUriNote(rootNote)}/${type}/${tonality}`

export const { useValue: useScale, provider: ScaleProvider } =
  getValueProviderSetup<Scale>("Scale")

export const useChangeScale = () => {
  const value = useScale()

  const { push } = useRouter()

  return useCallback(
    (params: Partial<Scale>) => {
      push(makeScalePath({ ...value, ...params }))
    },
    [push, value],
  )
}

Component Architecture

The scale page architecture is thoughtfully organized into four essential components, each serving a distinct purpose in our interactive learning environment. At its core, we have the ScaleManager for dynamically changing scales, the ScalePageTitle for clear scale identification, the ScaleNotes for interactive fretboard visualization, and the ScalePatterns for mastering the five fundamental scale positions. While we covered the fretboard rendering mechanics in our previous article here, this post delves into the specialized aspects of blues scale visualization.

import { VStack } from "@lib/ui/css/stack"
import { ScalePageTitle } from "./ScalePageTitle"
import { ScaleProvider } from "./state/scale"
import { ScaleNotes } from "./ScaleNotes"
import { PageContainer } from "../layout/PageContainer"
import { ScaleManager } from "./manage/ScaleManager"
import { ValueProp } from "@lib/ui/props"
import { Scale } from "@product/core/scale/Scale"
import { ScalePatterns } from "./patterns/ScalePatterns"

export const ScalePage = ({ value }: ValueProp<Scale>) => {
  return (
    <ScaleProvider value={value}>
      <PageContainer>
        <VStack gap={120}>
          <VStack gap={60}>
            <ScaleManager />
            <ScalePageTitle />
            <ScaleNotes />
          </VStack>
          <ScalePatterns />
        </VStack>
      </PageContainer>
    </ScaleProvider>
  )
}

Intuitive Scale Visualization

To help users grasp the blues scale intuitively, we leverage their likely familiarity with the pentatonic scale. Within the ScalePageTitle component, we display a descriptive subtitle that presents the blues scale as a natural extension of its pentatonic counterpart. For instance, when viewing an A minor blues scale, the subtitle would show "A minor pentatonic + D# (blue note)", making it clear that a blues scale is essentially a pentatonic scale with one additional note—the characteristic blue note that gives the scale its distinctive sound.

import { useScale } from "./state/scale"
import { getScaleName } from "@product/core/scale/getScaleName"
import { getBlueNote } from "@product/core/scale/blues/getBlueNote"
import { chromaticNotesNames } from "@product/core/note"

export const BluesScaleSubtitle = () => {
  const scale = useScale()

  const text = `${getScaleName({ ...scale, type: "pentatonic" })} + ${chromaticNotesNames[getBlueNote(scale)]} (blue note)`

  return text
}

Visual Note Differentiation

To enhance the visual distinction between different types of notes on the fretboard, we implement the ScaleNote component. This component determines how each note should be displayed based on its role in the scale. Root notes are rendered with primary emphasis, while the characteristic blue note receives special styling when playing a blues scale.

import { useMemo } from "react"
import { Note, NoteKind, NoteProps } from "../guitar/fretboard/Note"
import { useScale } from "./state/scale"
import { getBlueNote } from "@product/core/scale/blues/getBlueNote"
import { getNoteFromPosition } from "@product/core/note/getNoteFromPosition"
import { tuning } from "../guitar/config"

type ScaleNoteProps = Omit<NoteProps, "kind">

export const ScaleNote = (props: ScaleNoteProps) => {
  const { rootNote, type, tonality } = useScale()
  const note = getNoteFromPosition({ tuning, position: props })

  const kind: NoteKind = useMemo(() => {
    if (rootNote === note) {
      return "primary"
    }

    if (type === "blues" && note === getBlueNote({ rootNote, tonality })) {
      return "blue"
    }

    return "regular"
  }, [note, rootNote, tonality, type])

  return <Note {...props} kind={kind} />
}

Finding the Blue Note

The getBlueNote function uses set theory to identify the blue note of a scale. Rather than hardcoding note positions, it calculates the note by finding the difference between the blues scale and its pentatonic counterpart. This approach leverages the fact that a blues scale consists of a pentatonic scale plus one additional note. By generating both scale patterns and comparing their note sets, the function can determine the blue note programmatically.

import { difference } from "@lib/utils/array/difference"
import { getScaleNotes } from "../getScaleNotes"
import { Scale } from "../Scale"
import { scalePatterns } from "../ScaleType"

export const getBlueNote = (scale: Omit<Scale, "type">) => {
  const pentatonicNotes = getScaleNotes({
    rootNote: scale.rootNote,
    pattern: scalePatterns.pentatonic[scale.tonality],
  })

  const bluesNotes = getScaleNotes({
    rootNote: scale.rootNote,
    pattern: scalePatterns.blues[scale.tonality],
  })

  const [blueNote] = difference(pentatonicNotes, bluesNotes)

  return blueNote
}

Scale Pattern Implementation

While visualizing individual notes is valuable, guitarists typically learn scales through repeatable patterns across the fretboard. These patterns break down the scale into five manageable positions, making it easier to navigate the fretboard during improvisation. The ScalePatterns component implements this approach by displaying these essential positions:

import { useMemo } from "react"
import { useScale } from "../state/scale"
import { match } from "@lib/utils/match"
import { scalePatternsNumber } from "@product/core/scale/ScaleType"
import { range } from "@lib/utils/array/range"
import { getPentatonicPattern } from "@product/core/scale/pentatonic/getPentatonicPattern"
import { getBluesScalePattern } from "@product/core/scale/blues/getBluesScalePattern"
import { stringsCount, tuning } from "../../guitar/config"
import { VStack } from "@lib/ui/css/stack"
import { Text } from "@lib/ui/text"
import { ScalePattern } from "./ScalePattern"
import { getScaleName } from "@product/core/scale/getScaleName"

export const ScalePatterns = () => {
  const scale = useScale()

  const scaleName = getScaleName(scale)

  const patterns = useMemo(() => {
    const { type } = scale
    if (type === "full") return undefined

    const generate = match(type, {
      pentatonic: () => getPentatonicPattern,
      blues: () => getBluesScalePattern,
    })

    return range(scalePatternsNumber).map((index) =>
      generate({ index, scale, stringsCount, tuning }),
    )
  }, [scale])

  if (!patterns) return null

  return (
    <VStack gap={60}>
      <VStack gap={8}>
        <Text
          centerHorizontally
          weight={800}
          size={32}
          color="contrast"
          as="h2"
        >
          {scaleName} Patterns
        </Text>
        <Text
          centerHorizontally
          weight={700}
          size={20}
          color="supporting"
          as="h4"
        >
          {scalePatternsNumber} Essential Shapes for Guitar Solos
        </Text>
      </VStack>
      {patterns.map((pattern, index) => (
        <ScalePattern key={index} value={pattern} index={index} />
      ))}
    </VStack>
  )
}

Building Blues Patterns

We've already covered pentatonic patterns in the previous article. Building on this foundation, our blues scale patterns extend the pentatonic shapes by strategically inserting the blue note. The getBluesScalePattern function takes a pentatonic pattern and examines each note position, adding the blue note either one fret below the root or one fret above specific pentatonic notes.

import { getPentatonicPattern } from "../pentatonic/getPentatonicPattern"
import { Scale } from "../Scale"
import { NotePosition } from "../../note/NotePosition"
import { getNoteFromPosition } from "../../note/getNoteFromPosition"
import { normalizeFretPositions } from "../../note/normalizeFretPositions"
import { getBlueNote } from "./getBlueNote"

type Input = {
  index: number
  scale: Omit<Scale, "type">
  stringsCount: number
  tuning: number[]
}

export const getBluesScalePattern = (input: Input) => {
  const pentatonicPattern = getPentatonicPattern(input)

  const { scale, tuning } = input

  const blueNote = getBlueNote(scale)

  const result: NotePosition[] = []

  pentatonicPattern.forEach((position, noteIndex) => {
    result.push(position)

    const note = getNoteFromPosition({ position, tuning })
    if (noteIndex === 0 && note - 1 === blueNote) {
      result.push({
        string: position.string,
        fret: position.fret - 1,
      })
    } else if (
      noteIndex < pentatonicPattern.length - 1 &&
      note + 1 === blueNote
    ) {
      result.push({
        string: position.string,
        fret: position.fret + 1,
      })
    }
  })

  return normalizeFretPositions(result)
}

This implementation ensures that each pattern position remains intuitive to play while incorporating the essential blue note. By placing the blue note adjacent to existing pentatonic notes, guitarists can seamlessly transition between pentatonic and blues phrases, enriching their improvisational vocabulary.