Welcome to Part 8 of our guitar theory app series! Today, we'll expand our scales functionality by implementing five essential patterns that make learning complete major and minor scales more intuitive. These patterns provide a systematic approach to mastering scales across the fretboard. Check out the complete source code in the GitHub repository.
Instead of memorizing separate patterns for major and minor scales, we can leverage their relative relationship—a concept elegantly implemented in our code through the getFullScaleRelativeTonalityRootNote
function. This function calculates that relative scales share identical note patterns but with roots positioned three semitones apart; for instance, C major and A minor contain the same notes, with A being three semitones below C. Our implementation uses pattern matching to determine the direction of this three-semitone shift (upward when converting from minor to major, downward when moving from major to minor) and applies modulo arithmetic to ensure the resulting root note stays within our chromatic range, making scale navigation more efficient for guitarists learning to traverse the fretboard.
import { match } from "@lib/utils/match"
import { chromaticNotesNumber } from "../../note"
import { Scale } from "../Scale"
const semitones = 3
export const getFullScaleRelativeTonalityRootNote = ({
tonality,
rootNote,
}: Omit<Scale, "type">): number => {
const direction = match(tonality, {
minor: () => 1,
major: () => -1,
})
return (
(rootNote + semitones * direction + chromaticNotesNumber) %
chromaticNotesNumber
)
}
To display this relative relationship in our interface, we've implemented a RelativeScaleSubtitle
component that accepts a relativeRootNote
prop. This component renders a clickable link that seamlessly navigates users to the corresponding relative scale. While we also utilize this component with pentatonic scales, the underlying calculation for determining the relative root note differs in that context. For those interested in exploring how pentatonic patterns work, check out our detailed explanation in this post.
import { UnstyledButton } from "@lib/ui/buttons/UnstyledButton"
import { getColor } from "@lib/ui/theme/getters"
import { getPairComplement } from "@lib/utils/pair/getPairComplement"
import { getScaleName } from "@product/core/scale/getScaleName"
import { tonalities } from "@product/core/tonality"
import styled from "styled-components"
import { useChangeScale, useScale } from "./state/scale"
const Button = styled(UnstyledButton)`
&:hover {
color: ${getColor("textPrimary")};
}
`
export const RelativeScaleSubtitle = ({
relativeRootNote,
}: {
relativeRootNote: number
}) => {
const scale = useScale()
const changeScale = useChangeScale()
const relativeTonality = getPairComplement(tonalities, scale.tonality)
const relativeScale = {
...scale,
rootNote: relativeRootNote,
tonality: relativeTonality,
}
return (
<Button onClick={() => changeScale(relativeScale)}>
(same notes as in {getScaleName(relativeScale)})
</Button>
)
}
To represent scales in our app, we use a Scale
type with three key properties: a type (either full
, pentatonic
, or blues
), a tonality (major
or minor
), and a root note (an integer from 0 to 11 representing positions in the chromatic scale).
export const tonalities = ["major", "minor"] as const
export type Tonality = (typeof tonalities)[number]
export const scaleTypes = ["full", "pentatonic", "blues"] as const
export type ScaleType = (typeof scaleTypes)[number]
export type Scale = {
type: ScaleType
tonality: Tonality
rootNote: number
}
When a user selects a different scale, our app redirects them to a dedicated page for that selection. For those interested in exploring how scales are visualized on the fretboard, check out the first post in this series here.
import { getValueProviderSetup } from "@lib/ui/state/getValueProviderSetup"
import { toUriNote } from "@product/core/note/uriNote"
import { Scale } from "@product/core/scale/Scale"
import { useRouter } from "next/router"
import { useCallback } from "react"
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],
)
}
Now let's examine the UI implementation that displays these patterns. Our ScalePatterns
component creates a visual representation of the five pattern system, allowing guitarists to see how each pattern connects across the fretboard. The component dynamically generates patterns based on the currently selected scale using the getScalePattern
function, which calculates note positions for each pattern index.
import { VStack } from "@lib/ui/css/stack"
import { Text } from "@lib/ui/text"
import { range } from "@lib/utils/array/range"
import { getScaleName } from "@product/core/scale/getScaleName"
import { getScalePattern } from "@product/core/scale/getScalePattern"
import { scalePatternsNumber } from "@product/core/scale/ScaleType"
import { useMemo } from "react"
import { useScale } from "../state/scale"
import { ScalePattern } from "./ScalePattern"
export const ScalePatterns = () => {
const scale = useScale()
const scaleName = getScaleName(scale)
const patterns = useMemo(() => {
return range(scalePatternsNumber).map((index) =>
getScalePattern({ index, scale }),
)
}, [scale])
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>
)
}
Each scale has its own logic for calculating patterns. The getScalePattern
function serves as a resolver that selects the appropriate calculation function based on scale type and applies it with the necessary parameters.
import { Scale } from "../Scale"
import { ScaleType } from "../ScaleType"
import { getBluesScalePattern } from "./blues"
import { getFullScalePattern } from "./full"
import { getPentatonicPattern } from "./pentatonic"
import { ScalePatternResolver } from "./ScalePatternResolver"
const resolvers: Record<ScaleType, ScalePatternResolver> = {
blues: getBluesScalePattern,
full: getFullScalePattern,
pentatonic: getPentatonicPattern,
}
type RootScalePatternResolverInput = {
index: number
scale: Scale
}
export const getScalePattern = (input: RootScalePatternResolverInput) => {
const resolver = resolvers[input.scale.type]
return resolver(input)
}
Each pattern resolver function follows a consistent interface: it accepts both a scale object (with the type
property omitted since it's determined by the resolver) and a pattern index as inputs. The function processes these parameters and returns an array of NotePosition
objects that precisely map where notes should appear on the fretboard for that specific pattern.
import { NotePosition } from "../../note/NotePosition"
import { Scale } from "../Scale"
type ScalePatternResolverInput = {
index: number
scale: Omit<Scale, "type">
}
export type ScalePatternResolver = (
input: ScalePatternResolverInput,
) => NotePosition[]
To represent the precise location of notes on the guitar fretboard, we define a NotePosition
type with two key properties: a zero-based string
index and a fret
number. The fret value uses a specific convention where -1 indicates an open string (no finger pressed), 0 represents the first fret, and so on.
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
}
Our scale patterns are built on the CAGED chord system framework, with each pattern anchored to a specific chord shape—the first pattern corresponds to the E form as defined by firstMajorScalePatternChord
. While some note positions use fret index -2 (not physically playable in open positions), we shift these patterns up to the 12th fret area when needed for practical playability. This approach maintains the spatial relationship between patterns according to the CAGED sequence, allowing guitarists to smoothly connect all positions across the fretboard. For a deeper understanding of the CAGED system that underpins these patterns, check out our detailed explanation in this post.
import { CagedChord } from "../../chords/caged"
import { NotePosition } from "../../note/NotePosition"
export const majorScalePatterns: NotePosition[][] = [
[
{ string: 0, fret: 1 },
{ string: 0, fret: -1 },
{ string: 0, fret: -2 },
{ string: 1, fret: 1 },
{ string: 1, fret: -1 },
{ string: 2, fret: 1 },
{ string: 2, fret: 0 },
{ string: 2, fret: -2 },
{ string: 3, fret: 1 },
{ string: 3, fret: 0 },
{ string: 3, fret: -2 },
{ string: 4, fret: 1 },
{ string: 4, fret: -1 },
{ string: 4, fret: -2 },
{ string: 5, fret: 1 },
{ string: 5, fret: -1 },
{ string: 5, fret: -2 },
],
[
{ string: 0, fret: 2 },
{ string: 0, fret: 1 },
{ string: 0, fret: -1 },
{ string: 1, fret: 2 },
{ string: 1, fret: 1 },
{ string: 1, fret: -1 },
{ string: 2, fret: 1 },
{ string: 2, fret: -1 },
{ string: 2, fret: -2 },
{ string: 3, fret: 1 },
{ string: 3, fret: -1 },
{ string: 3, fret: -2 },
{ string: 4, fret: 1 },
{ string: 4, fret: -1 },
{ string: 5, fret: 2 },
{ string: 5, fret: 1 },
{ string: 5, fret: -1 },
],
[
{ string: 0, fret: 2 },
{ string: 0, fret: 0 },
{ string: 0, fret: -1 },
{ string: 1, fret: 2 },
{ string: 1, fret: 0 },
{ string: 1, fret: -1 },
{ string: 2, fret: 1 },
{ string: 2, fret: -1 },
{ string: 3, fret: 2 },
{ string: 3, fret: 1 },
{ string: 3, fret: -1 },
{ string: 4, fret: 2 },
{ string: 4, fret: 1 },
{ string: 4, fret: -1 },
{ string: 5, fret: 2 },
{ string: 5, fret: 0 },
{ string: 5, fret: -1 },
],
[
{ string: 0, fret: -1 },
{ string: 0, fret: 1 },
{ string: 1, fret: 1 },
{ string: 1, fret: 2 },
{ string: 1, fret: -1 },
{ string: 2, fret: 1 },
{ string: 2, fret: 0 },
{ string: 2, fret: -2 },
{ string: 3, fret: 1 },
{ string: 3, fret: -1 },
{ string: 3, fret: -2 },
{ string: 4, fret: 1 },
{ string: 4, fret: -1 },
{ string: 4, fret: -2 },
{ string: 5, fret: 1 },
{ string: 5, fret: -1 },
],
[
{ string: 0, fret: 2 },
{ string: 0, fret: 1 },
{ string: 0, fret: -1 },
{ string: 1, fret: 2 },
{ string: 1, fret: 0 },
{ string: 1, fret: -1 },
{ string: 2, fret: 1 },
{ string: 2, fret: -1 },
{ string: 2, fret: -2 },
{ string: 3, fret: 1 },
{ string: 3, fret: -1 },
{ string: 4, fret: 2 },
{ string: 4, fret: 1 },
{ string: 4, fret: -1 },
{ string: 5, fret: 2 },
{ string: 5, fret: 1 },
{ string: 5, fret: -1 },
],
]
export const firstMajorScalePatternChord: CagedChord = "e"
To implement this automatic octave shifting, our normalizeFretPositions
utility detects patterns with fret values below -1 and transposes all notes up by exactly one octave (12 frets), preserving their relative relationships while making them physically playable on the guitar.
import { NotePosition } from "./NotePosition"
import { shiftNotePositions } from "./shiftNotePositions"
import { chromaticNotesNumber } from "."
export const normalizeFretPositions = (
positions: NotePosition[],
): NotePosition[] => {
if (positions.some((position) => position.fret < -1)) {
return shiftNotePositions(positions, chromaticNotesNumber)
}
return positions
}
For repositioning scale patterns across the fretboard, we implement the shiftNotePositions
function, which transposes all notes in a pattern by a specified fret offset.
import { NotePosition } from "./NotePosition"
export const shiftNotePositions = (
positions: NotePosition[],
offset: number,
) => {
return positions.map((position) => ({
...position,
fret: position.fret + offset,
}))
}
The getFullScalePattern
function handles positioning scale patterns on the fretboard with a straightforward approach. For minor scales, it uses the relative tonality system to convert them to their equivalent major patterns. With major scales, it retrieves the base pattern, rotates the CAGED system distances to maintain proper spacing, and calculates an offset based on the root note, standard tuning, and distance from the first pattern. After shifting the notes by this offset, it ensures all positions are playable through normalization. This method creates a system where the five patterns connect naturally across the fretboard.
import { getLastItem } from "@lib/utils/array/getLastItem"
import { rotateArray } from "@lib/utils/array/rotateArray"
import { sum } from "@lib/utils/array/sum"
import { normalizeFretPositions } from "@product/core/note/normalizeFretPositions"
import { cagedChords, cagedTemplateDistances } from "../../chords/caged"
import { standardTuning } from "../../guitar/tuning"
import { shiftNotePositions } from "../../note/shiftNotePositions"
import { getFullScaleRelativeTonalityRootNote } from "../full/getFullScaleRelativeTonalityRootNote"
import {
firstMajorScalePatternChord,
majorScalePatterns,
} from "../full/majorScalePatterns"
import { ScalePatternResolver } from "./ScalePatternResolver"
export const getFullScalePattern: ScalePatternResolver = (input) => {
if (input.scale.tonality === "minor") {
return getFullScalePattern({
...input,
scale: {
...input.scale,
tonality: "major",
rootNote: getFullScaleRelativeTonalityRootNote(input.scale),
},
})
}
const pattern = majorScalePatterns[input.index]
const distances = rotateArray(
cagedTemplateDistances.major,
cagedChords.indexOf(firstMajorScalePatternChord),
)
const offset =
input.scale.rootNote -
getLastItem(standardTuning) +
sum(distances.slice(0, input.index))
const positionedPattern = shiftNotePositions(pattern, offset)
return normalizeFretPositions(positionedPattern)
}
The ScalePattern
component visualizes fretboard patterns by rendering note positions calculated by the getScalePattern
function, leveraging the core fretboard visualization components introduced in the first post of this series to create an interactive representation of each scale shape.
import { VStack } from "@lib/ui/css/stack"
import { IndexProp, ValueProp } from "@lib/ui/props"
import { NotePosition } from "@product/core/note/NotePosition"
import { getScaleName } from "@product/core/scale/getScaleName"
import { Fretboard } from "../../guitar/fretboard/Fretboard"
import { SectionTitle } from "../../ui/SectionTitle"
import { ScaleNote } from "../ScaleNote"
import { useScale } from "../state/scale"
export const ScalePattern = ({
value,
index,
}: ValueProp<NotePosition[]> & IndexProp) => {
const scale = useScale()
const scaleName = getScaleName(scale)
const title = `${scaleName} Pattern #${index + 1}`
return (
<VStack gap={40}>
<SectionTitle>{title}</SectionTitle>
<Fretboard>
{value.map((position) => {
return (
<ScaleNote
key={`${position.string}-${position.fret}`}
{...position}
/>
)
})}
</Fretboard>
</VStack>
)
}
By implementing these five interconnected scale patterns based on the CAGED system, our app provides guitarists with a systematic approach to mastering scales across the entire fretboard, transforming what was once a daunting memorization task into an intuitive learning journey.