Discover how to build an interactive guitar fretboard that visualizes Major and Minor pentatonic scales using React and TypeScript. This step-by-step guide will walk you through creating an intuitive widget that helps guitarists learn and practice these essential scales. You can explore the live demo at pentafret.com or dive into the complete source code on GitHub.
Our application allows users to explore guitar scales interactively by selecting a root note and scale type. The app then visualizes the corresponding notes across 15 frets on the fretboard. For a detailed walkthrough of the app's foundation and the core fretboard rendering implementation, check out this post where we cover those topics in depth.
One of the most powerful aspects of pentatonic scales is their symmetrical nature - the pattern remains identical for both Major and Minor scales, with only the root note position shifting. For example, compare the G Major pentatonic with the E Minor pentatonic scales - you'll notice the same finger patterns, just starting from different positions. This means that once you master the 5 basic pentatonic patterns, you can apply them to both Major and Minor scales by simply moving the patterns to different positions on the fretboard. This versatility makes pentatonic scales an incredibly efficient learning tool for guitarists, as the same finger patterns can be used to play in any key, Major or Minor.
To enhance the learning experience, we want to highlight the relationship between Major and Minor pentatonic scales that share the same pattern. When a user selects either a Major or Minor pentatonic scale, we display a clickable subtitle that shows its relative counterpart (e.g., "same pattern as E Minor pentatonic" for G Major pentatonic). Clicking this subtitle instantly switches to the related scale, allowing users to visually understand how the same pattern can be used for both scales.
import { UnstyledButton } from "@lib/ui/buttons/UnstyledButton"
import { useChangeScale, useScale } from "./state/scale"
import { chromaticNotesNames } from "@product/core/note"
import { PentatonicScale, scaleNames } from "@product/core/scale"
import styled from "styled-components"
import { getColor } from "@lib/ui/theme/getters"
import { getRelativePentatonic } from "@product/core/scale/getRelativePentatonic"
const Button = styled(UnstyledButton)`
&:hover {
color: ${getColor("textPrimary")};
}
`
export const PentatonicSubtitle = ({ scale }: { scale: PentatonicScale }) => {
const { rootNote } = useScale()
const changeScale = useChangeScale()
const relativePentatonic = getRelativePentatonic({
scale,
rootNote,
})
return (
<Button onClick={() => changeScale(relativePentatonic)}>
(same pattern as {chromaticNotesNames[relativePentatonic.rootNote]}{" "}
{scaleNames[relativePentatonic.scale]} pentatonic)
</Button>
)
}
The getRelativePentatonic
function is the core calculation engine behind this feature. For any given scale, it determines its relative counterpart by shifting the root note by three semitones - up when converting from Minor and down for Major. For instance, this is how we determine that G Major and E Minor share the same pattern.
import { match } from "@lib/utils/match"
import { chromaticNotesNumber } from "../note"
import { getPairComplement } from "@lib/utils/pair/getPairComplement"
import { scalePatterns, pentatonicScales, PentatonicScale } from "./index"
type Pentatonic = {
scale: PentatonicScale
rootNote: number
}
export const getRelativePentatonic = ({
scale,
rootNote,
}: Pentatonic): Pentatonic => {
const [semitones] = scalePatterns["minor-pentatonic"]
const direction = match(scale, {
"minor-pentatonic": () => 1,
"major-pentatonic": () => -1,
})
const relativeNote =
(rootNote + semitones * direction + chromaticNotesNumber) %
chromaticNotesNumber
const relativeScale = getPairComplement(pentatonicScales, scale)
return {
scale: relativeScale,
rootNote: relativeNote,
}
}
In our application, we represent musical notes using a zero-based numbering system. Each note is assigned a number from 0 to 11, where A is 0, A# (or Bb) is 1, B is 2, C is 3, and so forth.
import { scalePatterns } from "../scale"
export const naturalNotesNames = ["A", "B", "C", "D", "E", "F", "G"]
export const chromaticNotesNames = scalePatterns.minor.reduce(
(acc, step, index) => {
const note = naturalNotesNames[index]
acc.push(note)
if (step === 2) {
acc.push(`${note}#`)
}
return acc
},
[] as string[],
)
export const chromaticNotesNumber = chromaticNotesNames.length
export const isNaturalNote = (note: number) =>
chromaticNotesNames[note].length === 1
To present the scale patterns in an organized and visually appealing way, we create a PentatonicPatterns
component. This component displays a collection of essential scale shapes, each representing a different position on the fretboard. It shows a title with the current scale name (e.g., "G Major Pentatonic Patterns") and renders multiple PentatonicPattern
components, one for each shape in the selected scale:
import { range } from "@lib/utils/array/range"
import { scaleNames, PentatonicScale, scalePatterns } from "@product/core/scale"
import { Text } from "@lib/ui/text"
import { chromaticNotesNames } from "@product/core/note"
import { useScale } from "../state/scale"
import { VStack } from "@lib/ui/css/stack"
import { PentatonicPattern } from "./PentatonicPattern"
export const PentatonicPatterns = ({ scale }: { scale: PentatonicScale }) => {
const { rootNote } = useScale()
const noteName = chromaticNotesNames[rootNote]
const scaleName = scaleNames[scale]
const title = `${noteName} ${scaleName} Pentatonic Patterns`
return (
<VStack gap={60}>
<VStack gap={8}>
<Text
centerHorizontally
weight={800}
size={32}
color="contrast"
as="h2"
>
{title}
</Text>
<Text
centerHorizontally
weight={700}
size={20}
color="supporting"
as="h4"
>
{scalePatterns[scale].length} Essential Shapes for Guitar Solos
</Text>
</VStack>
{range(scalePatterns[scale].length).map((index) => (
<PentatonicPattern key={index} index={index} scale={scale} />
))}
</VStack>
)
}
Each pattern is rendered using the PentatonicPattern
component, which creates an interactive fretboard visualization for a specific position. The component takes a pattern index and scale type as props, calculates the note positions using getPentatonicPattern
, and renders them on a fretboard. Root notes are highlighted to help guitarists identify key reference points while practicing:
import { IndexProp } from "@lib/ui/props"
import { useScale } from "../state/scale"
import { PentatonicScale, scaleNames } from "@product/core/scale"
import { chromaticNotesNames } from "@product/core/note"
import { stringsCount, tuning } from "../../guitar/config"
import { Fretboard } from "../../guitar/fretboard/Fretboard"
import { Note } from "../../guitar/fretboard/Note"
import { VStack } from "@lib/ui/css/stack"
import { Text } from "@lib/ui/text"
import { getNoteFromPosition } from "@product/core/note/getNoteFromPosition"
import { getPentatonicPattern } from "./getPentatonicPattern"
export const PentatonicPattern = ({
index: patternIndex,
scale,
}: IndexProp & { scale: PentatonicScale }) => {
const { rootNote } = useScale()
const notes = getPentatonicPattern({
index: patternIndex,
scale,
rootNote,
stringsCount,
tuning,
})
const noteName = chromaticNotesNames[rootNote]
const scaleName = scaleNames[scale]
const title = `${noteName} ${scaleName} Pentatonic Pattern #${patternIndex + 1}`
return (
<VStack gap={40}>
<Text centerHorizontally color="contrast" as="h3" weight="700" size={18}>
{title}
</Text>
<Fretboard>
{notes.map((position) => {
const note = getNoteFromPosition({ tuning, position })
return (
<Note
key={`${position.string}-${position.fret}`}
{...position}
kind={rootNote === note ? "primary" : "regular"}
/>
)
})}
</Fretboard>
</VStack>
)
}
To reference a note's position on the fretboard, we need two pieces of information: the string number (zero-based, from top to bottom) and the fret number (where -1 represents an open string, and 0 represents the first fret):
export type NotePosition = {
// 0-based index of the string
string: number
// -1 if the note is open
// 0 if the note is on the 1st fret
fret: number
}
The getPentatonicPattern
function builds each scale shape using a systematic approach. First, it determines the starting note by adding up the scale intervals based on the pattern index. Then, it follows a specific traversal strategy: starting from the lowest string (6th), it places two notes per string, working its way up to the highest string (1st). For each new note, it calculates the fret position by either using the first note of the pattern or by adding the appropriate interval to the previous note's position. When moving to a new string, the algorithm applies a shift of 4 or 5 frets (depending on the string) to maintain the pattern's shape:
import { scalePatterns } from "@product/core/scale"
import { sum } from "@lib/utils/array/sum"
import { match } from "@lib/utils/match"
import { getRelativePentatonic } from "@product/core/scale/pentatonic/getRelativePentatonic"
import { PentatonicScale } from "@product/core/scale"
import { getLastItem } from "@lib/utils/array/getLastItem"
import { range } from "@lib/utils/array/range"
import { getNoteFret } from "@product/core/guitar/getNoteFret"
import { chromaticNotesNumber } from "@product/core/note"
import { NotePosition } from "@product/core/note/NotePosition"
type Input = {
index: number
scale: PentatonicScale
rootNote: number
stringsCount: number
tuning: number[]
}
export const getPentatonicPattern = ({
index,
scale,
rootNote,
stringsCount,
tuning,
}: Input) => {
const pattern = scalePatterns["minor-pentatonic"]
const minorRootNote = match(scale, {
"minor-pentatonic": () => rootNote,
"major-pentatonic": () =>
getRelativePentatonic({ scale, rootNote }).rootNote,
})
const firstNote =
(minorRootNote + sum(pattern.slice(0, index))) % chromaticNotesNumber
const result: NotePosition[] = []
range(stringsCount * 2).forEach((index) => {
const string = stringsCount - Math.floor(index / 2) - 1
const openNote = tuning[string]
const previousPosition = getLastItem(result)
const getFret = () => {
if (!previousPosition) {
return getNoteFret({ openNote, note: firstNote })
}
const step = pattern[(index + index - 1) % pattern.length]
const fret = previousPosition.fret + step
if (index % 2 === 0) {
const shift = string === 1 ? 4 : 5
return fret - shift
}
return fret
}
result.push({
string,
fret: getFret(),
})
})
if (result.some((position) => position.fret < -1)) {
return result.map((position) => ({
...position,
fret: position.fret + chromaticNotesNumber,
}))
}
return result
}
Sometimes, our pattern calculation might result in negative fret positions (less than -1), which aren't playable on a guitar. In such cases, we shift the entire pattern up by an octave (12 frets) to keep it in a playable range. For example, in the G Minor pentatonic pattern shown below, we move all notes 12 frets higher to maintain the same pattern shape in a more practical position:
With these components and algorithms in place, we've created an interactive learning tool that helps guitarists visualize and practice pentatonic scales across the fretboard. The application handles the complexities of musical theory and fretboard geometry, allowing players to focus on mastering these essential patterns in any key they choose.