This is part 9 of the series where we build a guitar theory app with React. Today we'll add a new page for learning triads – three-note chords that form the foundation of harmony in music. Triads are essential for understanding chord progressions, creating accompaniments, and building more complex chord structures. By mastering triads, guitarists gain a deeper understanding of how music works harmonically. You can find all the code in the GitHub repository.
Since most guitarists come to triads after learning the major scale, it's more intuitive to understand triads within the framework of the 5 major scale patterns. Triads are three-note chords built from the 1st (root), 3rd, and 5th degrees of a scale. These powerful building blocks form the foundation of Western harmony and are essential for chord construction across all musical genres.
When we overlay triads onto familiar major scale patterns, guitarists can immediately see how these chord structures relate to scales they already know. This approach has several advantages: it reinforces scale knowledge, reveals the harmonic relationship between scales and chords, and provides a systematic way to locate triads across the entire fretboard. Each major scale pattern contains all seven diatonic triads (I, ii, iii, IV, V, vi, vii°), giving guitarists a complete harmonic vocabulary within each position.
import { capitalizeFirstLetter } from "@lib/utils/capitalizeFirstLetter"
export const triadIntervals = [1, 3, 5]
export const triadRomanNumerals = ["I", "ii", "iii", "IV", "V", "vi", "vii°"]
type ChordQuality = "major" | "minor" | "diminished"
export const chordQualities: ChordQuality[] = [
"major",
"minor",
"minor",
"major",
"major",
"minor",
"diminished",
]
export const diatonicTriadsNumber = triadRomanNumerals.length
export const getTriadName = (index: number) => {
const numberal = triadRomanNumerals[index]
const quality = chordQualities[index]
return [
numberal,
quality === "major" ? capitalizeFirstLetter(quality) : quality,
"Triad",
].join(" ")
}
The getTriadName
function constructs formatted names for each triad in the diatonic scale. It takes an index (0-6) and combines the corresponding Roman numeral (I, ii, etc.), chord quality (major, minor, diminished), and the word "Triad." Major qualities are capitalized, resulting in names like "I Major Triad" or "ii minor Triad" that clearly identify each chord's position and characteristic sound within the scale.
To efficiently render our triad learning experience, we'll use Next.js static site generation to pre-build pages for every possible combination of root notes and triad positions. The following code creates static pages for all 12 chromatic notes combined with all 7 diatonic triads, resulting in 84 unique pages that load instantly without server-side processing. This approach maximizes performance while keeping our routing structure clean and SEO-friendly.
import { range } from "@lib/utils/array/range"
import { chromaticNotesNumber } from "@product/core/note"
import { fromUriNote, toUriNote } from "@product/core/note/uriNote"
import { diatonicTriadsNumber } from "@product/core/triads"
import { GetStaticPaths, GetStaticProps } from "next"
import { TriadPage } from "../../../../triad/TriadPage"
export default TriadPage
type Params = {
index: string
rootNote: string
}
export const getStaticPaths: GetStaticPaths<Params> = async () => {
const paths = range(chromaticNotesNumber).flatMap((rootNote) =>
range(diatonicTriadsNumber).map((index) => ({
params: {
index: index.toString(),
rootNote: toUriNote(rootNote),
},
})),
)
return {
paths,
fallback: false,
}
}
export const getStaticProps: GetStaticProps = async ({ params }) => {
const { rootNote, index } = params as Params
return {
props: {
value: {
rootNote: fromUriNote(rootNote),
index: Number(index),
},
},
}
}
We will use TriadProvider
to allow children components of the TriadPage
to access the triad state directly, eliminating prop drilling. When a user selects a different triad index or root note, the useChangeTriad
hook handles navigation to the corresponding page, maintaining our routing structure.
import { getValueProviderSetup } from "@lib/ui/state/getValueProviderSetup"
import { toUriNote } from "@product/core/note/uriNote"
import { useRouter } from "next/router"
import { useCallback } from "react"
export type TriadState = {
index: number
rootNote: number
}
export const makeTriadPath = ({ index, rootNote }: TriadState) =>
`/triad/${[index, toUriNote(rootNote)].join("/")}`
export const { useValue: useTriad, provider: TriadProvider } =
getValueProviderSetup<TriadState>("Triad")
export const useChangeTriad = () => {
const value = useTriad()
const { push } = useRouter()
return useCallback(
(params: Partial<TriadState>) => {
push(makeTriadPath({ ...value, ...params }))
},
[push, value],
)
}
The TriadPage
component serves as the central organizing structure for our triad learning interface. It composes all the UI elements into a cohesive layout where guitarists can explore triads within each major scale pattern. The component uses a nested VStack
structure to create visual hierarchy, separating the controls for root note and triad selection from the main content area. This separation helps players focus on one scale pattern at a time while maintaining access to navigation controls. The component renders each of the five major scale patterns in sequence, displaying how the selected triad fits within each pattern across the fretboard.
import { VStack } from "@lib/ui/css/stack"
import { ValueProp } from "@lib/ui/props"
import { range } from "@lib/utils/array/range"
import { scalePatternsNumber } from "@product/core/scale/ScaleType"
import { PageContainer } from "../layout/PageContainer"
import { ManageTriadIndex } from "./manage/ManageTriadIndex"
import { ManageTriadRootNote } from "./manage/ManageTriadRootNote"
import { TriadProvider, TriadState } from "./state/triad"
import { TriadOnMajorScalePattern } from "./TriadOnMajorScalePattern"
import { TriadPageTitle } from "./TriadPageTitle"
export const TriadPage = ({ value }: ValueProp<TriadState>) => (
<TriadProvider value={value}>
<PageContainer>
<VStack gap={120}>
<VStack gap={60}>
<VStack gap={20} alignItems="center">
<VStack alignItems="start" gap={20}>
<ManageTriadRootNote />
<ManageTriadIndex />
</VStack>
</VStack>
<TriadPageTitle />
</VStack>
{range(scalePatternsNumber).map((index) => (
<TriadOnMajorScalePattern key={index} scalePatternIndex={index} />
))}
</VStack>
</PageContainer>
</TriadProvider>
)
The ManageTriadRootNote
and ManageTriadIndex
components both leverage the versatile GroupedRadioInput
from RadzionKit, creating a consistent interface for musicians to select either root notes or triad positions.
import { GroupedRadioInput } from "@lib/ui/inputs/GroupedRadioInput"
import { InputContainer } from "@lib/ui/inputs/InputContainer"
import { InputLabel } from "@lib/ui/inputs/InputLabel"
import { range } from "@lib/utils/array/range"
import { chromaticNotesNames, chromaticNotesNumber } from "@product/core/note"
import { useChangeTriad } from "../state/triad"
import { useTriad } from "../state/triad"
export const ManageTriadRootNote = () => {
const { rootNote } = useTriad()
const setValue = useChangeTriad()
return (
<InputContainer>
<InputLabel>
Major scale root note: {chromaticNotesNames[rootNote]}
</InputLabel>
<GroupedRadioInput
value={chromaticNotesNames[rootNote]}
onChange={(noteName) => {
const index = chromaticNotesNames.indexOf(noteName)
setValue({ rootNote: index })
}}
options={range(chromaticNotesNumber).map(
(index) => chromaticNotesNames[index],
)}
renderOption={(noteName) => noteName}
/>
</InputContainer>
)
}
The TriadPageTitle
component generates dynamic page titles and metadata based on the currently selected triad and root note. It extracts the root note name and triad information from the application state, then constructs formatted title and description strings that clearly identify the specific triad being studied. This component ensures our pages have meaningful titles for both users and search engines, helping guitarists understand exactly which triad and scale they're currently viewing.
import { PageMetaTags } from "@lib/next-ui/metadata/PageMetaTags"
import { chromaticNotesNames } from "@product/core/note"
import { getTriadName, triadRomanNumerals } from "@product/core/triads"
import { PageTitle } from "../ui/PageTitle"
import { useTriad } from "./state/triad"
export const TriadPageTitle = () => {
const { rootNote, index } = useTriad()
const rootNoteName = chromaticNotesNames[rootNote]
const triadName = getTriadName(index)
const romanNumeral = triadRomanNumerals[index]
const title = `${rootNoteName} Major Scale - ${triadName} | Guitar Fretboard Patterns`
const description = `Interactive guide to the ${triadName} (${romanNumeral}) in the ${rootNoteName} major scale. Learn how this triad appears across all standard scale patterns on the guitar fretboard.`
return (
<>
<PageMetaTags title={title} description={description} />
<PageTitle>
{rootNoteName} Major Scale - {triadName}
</PageTitle>
</>
)
}
To render each of the 5 major scale patterns, we utilize the getFullScalePattern
function. Since scale patterns deserve thorough explanation, there is a separate post covering both major and minor scale patterns here.
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 TriadOnMajorScalePattern
component visualizes how triads fit within each major scale pattern on the fretboard. For each pattern, it first retrieves the complete set of note positions using getFullScalePattern
. Then it calculates the corresponding scale notes and rotates them based on the selected triad index—this rotation shifts our perspective to view the scale from the triad's root position. Each note on the fretboard is then assigned a scale degree (1-7), and the component highlights triad notes (degrees 1, 3, and 5) with a "primary" styling while rendering regular scale tones with standard styling. This creates a clear visual distinction between the three-note triad structure and the surrounding scale context, allowing guitarists to instantly recognize these chord tones within familiar scale patterns.
import { VStack } from "@lib/ui/css/stack"
import { rotateArray } from "@lib/utils/array/rotateArray"
import { getNoteFromPosition } from "@product/core/note/getNoteFromPosition"
import { getScaleName } from "@product/core/scale/getScaleName"
import { getScaleNotes } from "@product/core/scale/getScaleNotes"
import { getFullScalePattern } from "@product/core/scale/getScalePattern/full"
import { Scale } from "@product/core/scale/Scale"
import { scalePatterns } from "@product/core/scale/ScaleType"
import { getTriadName, triadIntervals } from "@product/core/triads"
import { Fretboard } from "../guitar/fretboard/Fretboard"
import { Note } from "../guitar/fretboard/Note"
import { SectionTitle } from "../ui/SectionTitle"
import { useTriad } from "./state/triad"
export const TriadOnMajorScalePattern = ({
scalePatternIndex,
}: {
scalePatternIndex: number
}) => {
const { rootNote, index: triadIndex } = useTriad()
const scale: Scale = {
tonality: "major",
rootNote,
type: "full",
}
const scalePattern = getFullScalePattern({
scale,
index: scalePatternIndex,
})
const scaleNotes = rotateArray(
getScaleNotes({
rootNote,
pattern: scalePatterns.full[scale.tonality],
}),
triadIndex,
)
const title = `${getTriadName(triadIndex)} on ${getScaleName(scale)} Pattern #${
scalePatternIndex + 1
}`
return (
<VStack gap={40}>
<SectionTitle>{title}</SectionTitle>
<Fretboard>
{scalePattern.map((position) => {
const scaleDegree =
scaleNotes.indexOf(getNoteFromPosition({ position })) + 1
const isTriadNote = triadIntervals.includes(scaleDegree)
return (
<Note
key={`${position.string}-${position.fret}`}
{...position}
kind={isTriadNote ? "primary" : undefined}
>
{scaleDegree}
</Note>
)
})}
</Fretboard>
</VStack>
)
}
By mastering triads across the five major scale patterns, you'll develop a comprehensive understanding of chord construction that transfers to any musical context, allowing you to create more sophisticated harmonies and improvise with greater harmonic awareness throughout the entire fretboard.