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.
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
}
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
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],
)
}
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>
)
}
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
}
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} />
}
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
}
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>
)
}
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.