From Theory to Fretboard: Dynamic Guitar Scale Patterns

From Theory to Fretboard: Dynamic Guitar Scale Patterns

April 6, 2025

12 min read

From Theory to Fretboard: Dynamic Guitar Scale Patterns

Introduction

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.

5 Scale Patterns
5 Scale Patterns

Implementing Relative Scale Relationships

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
  )
}

Building the UI for Scale Relationship Navigation

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>
  )
}

Defining the Scale Type System

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
}

Implementing Scale Navigation

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

Creating the Scale Patterns UI Component

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>
  )
}

Pattern Resolution System

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)
}

Designing the Pattern Resolver Interface

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[]

Representing Note Positions

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
}

Implementing the CAGED System for Scale Patterns

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"

Normalizing Note Positions for Playability

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
}

Shifting Scale Patterns Across the Fretboard

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,
  }))
}

Calculating Scale Patterns Dynamically

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)
}

Visualizing Scale Patterns on the Fretboard

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>
  )
}

Conclusion

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.