Welcome to the seventh installment in our React-based guitar theory application series. In this post, we'll explore the CAGED system further by implementing a dedicated page that visualizes chord templates for each of the five foundational CAGED shapes. As always, you can explore the complete implementation in the GitHub repository.
The CAGED template is a powerful system that connects five major chord forms across the fretboard, allowing you to play any major chord in multiple positions. Starting with an open chord (like C), you can systematically move up the fretboard by transitioning through each form in the sequence: C-A-G-E-D. Each transition involves replacing your ring finger with your index finger to form a barre chord, maintaining the same chord but in a different position. This creates a continuous chain of the same chord that repeats every octave, giving you complete fretboard access. For example, a C major chord can be played using all five forms, starting with the open C shape and progressing through A, G, E, and D forms, each time moving higher on the neck while still playing C major.
To manage the page's state, we'll use a straightforward TypeScript interface that captures two key aspects: the visualization type and the selected chord. The view can toggle between chord and arpeggio displays, building upon the arpeggio visualization concepts we explored in our previous post. When no specific chord is selected, the page displays an overview of all five CAGED shapes in either chord or arpeggio form, providing a comprehensive view of the system.
export const cagedViews = ["chord", "arpeggio"] as const
export type CagedView = (typeof cagedViews)[number]
export const cagedChords = ["c", "a", "g", "e", "d"] as const
export type CagedChord = (typeof cagedChords)[number]
export type CagedState = {
view: CagedView
chord?: CagedChord
}
Our application uses NextJS's static site generation to create optimized, pre-rendered pages at build time. We'll implement two dynamic routes: caged/[view]/index.tsx
for the overview page that displays all CAGED shapes in a comprehensive grid layout, and caged/[view]/[chord]/index.tsx
for the template page that presents detailed positions for a specific chord. While we covered the overview page implementation in our previous post, this post will focus on the template page that provides an in-depth look at each CAGED shape for a specific chord.
To implement these dynamic routes in NextJS, we'll use static site generation through the getStaticPaths
and getStaticProps
functions. The getStaticPaths
function generates all possible combinations of views (chord or arpeggio) and CAGED shapes, ensuring each combination has its own pre-rendered page. Meanwhile, getStaticProps
extracts the view and chord parameters from the URL to provide the necessary state for rendering the page components. This approach optimizes performance by pre-building all possible template variations at build time rather than generating them on demand.
import { cagedViews, cagedChords } from "@product/core/chords/caged"
import { GetStaticPaths, GetStaticProps } from "next"
import { CagedPage } from "../../../../caged/CagedPage"
import { CagedTemplateState } from "../../../../caged/template/state/cagedTemplate"
export default CagedPage
export const getStaticPaths: GetStaticPaths<CagedTemplateState> = async () => {
const paths = cagedViews.flatMap((view) =>
cagedChords.map((chord) => ({
params: {
view,
chord,
},
})),
)
return {
paths,
fallback: false,
}
}
export const getStaticProps: GetStaticProps = async ({ params }) => {
const { view, chord } = params as CagedTemplateState
return {
props: {
value: {
view,
chord,
},
},
}
}
To manage state across our application, we implement a centralized state management approach that leverages Next.js routing and React context. The following code establishes our core state management pattern for the CAGED system. It creates a typed provider that makes the current view and selected chord available throughout the component tree, while also providing a convenient navigation function that updates both the UI state and URL path simultaneously.
import { getValueProviderSetup } from "@lib/ui/state/getValueProviderSetup"
import { withoutUndefined } from "@lib/utils/array/withoutUndefined"
import { CagedChord, CagedView } from "@product/core/chords/caged"
import { useRouter } from "next/router"
import { useCallback } from "react"
export type CagedState = {
view: CagedView
chord?: CagedChord
}
export const makeCagedPath = ({ view, chord }: CagedState) =>
`/caged/${withoutUndefined([view, chord]).join("/")}`
export const { useValue: useCaged, provider: CagedProvider } =
getValueProviderSetup<CagedState>("Caged")
export const useChangeCaged = () => {
const value = useCaged()
const { push } = useRouter()
return useCallback(
(params: Partial<CagedState>) => {
push(makeCagedPath({ ...value, ...params }))
},
[push, value],
)
}
For template pages where chord selection is always required, we implement a specialized hook that transforms our optional chord field into a required one. This utility leverages TypeScript's type system through the RequiredFields
helper to ensure that components using this hook can safely access the chord without optional chaining. When template components use this hook, they get type-safe access to chord data with the guarantee that it will be present, simplifying their implementation by removing the need to handle the undefined case.
import { shouldBePresent } from "@lib/utils/assert/shouldBePresent"
import { RequiredFields } from "@lib/utils/types/RequiredFields"
import { useMemo } from "react"
import { CagedState, useCaged } from "../../state/caged"
export type CagedTemplateState = RequiredFields<CagedState, "chord">
export const useCagedTemplate = () => {
const caged = useCaged()
return useMemo(
() => ({
...caged,
chord: shouldBePresent(caged.chord),
}),
[caged],
)
}
The main CagedPage
component serves as a unified container for both overview and template views, conditionally rendering different content based on whether a specific chord is selected. This component leverages our state management system to provide consistent navigation between views while maintaining a clean component hierarchy. The layout consists of navigation controls at the top, followed by contextual titles, and finally the main content area that displays either the detailed template for a single chord or the overview grid of all CAGED shapes. This approach allows users to seamlessly switch between broad concept exploration and detailed chord study without disrupting the interface's overall structure or requiring separate page implementations.
import { HStack, VStack } from "@lib/ui/css/stack"
import { ValueProp } from "@lib/ui/props"
import { cagedChords } from "@product/core/chords/caged"
import { PageContainer } from "../layout/PageContainer"
import { CagedItem } from "./CagedItem"
import { CagedPageTitle } from "./CagedPageTitle"
import { ManageCagedChordPresense } from "./manage/ManageCagedChordPresense"
import { ManageCagedView } from "./manage/ManageCagedView"
import { CagedProvider, CagedState } from "./state/caged"
import { CagedTemplate } from "./template/CagedTemplate"
import { CagedTemplatePageTitle } from "./template/CagedTemplatePageTitle"
import { ManageCagedChord } from "./template/manage/ManageCagedChord"
export const CagedPage = ({ value }: ValueProp<CagedState>) => (
<CagedProvider value={value}>
<PageContainer>
<VStack gap={60}>
<VStack gap={20} alignItems="center">
<HStack wrap="wrap" gap={20}>
<ManageCagedView />
<ManageCagedChordPresense />
</HStack>
{value.chord && <ManageCagedChord />}
</VStack>
{value.chord ? <CagedTemplatePageTitle /> : <CagedPageTitle />}
{value.chord ? (
<CagedTemplate />
) : (
<HStack
fullWidth
gap={60}
alignItems="center"
justifyContent="center"
wrap="wrap"
>
{cagedChords.map((chord) => (
<CagedItem key={chord} value={chord} />
))}
</HStack>
)}
</VStack>
</PageContainer>
</CagedProvider>
)
For our shape selection controls, we implement GroupedRadioInput
from RadzionKit, providing an intuitive way for users to switch between the five CAGED chord shapes. This same radio-style selection pattern extends to both the ManageCagedView
and ManageCagedChordPresense
components.
import { GroupedRadioInput } from "@lib/ui/inputs/GroupedRadioInput"
import { capitalizeFirstLetter } from "@lib/utils/capitalizeFirstLetter"
import { cagedChords } from "@product/core/chords/caged"
import { useChangeCaged } from "../../state/caged"
import { useCagedTemplate } from "../state/cagedTemplate"
export const ManageCagedChord = () => {
const { chord } = useCagedTemplate()
const setValue = useChangeCaged()
return (
<GroupedRadioInput
value={chord}
onChange={(chord) => setValue({ chord })}
options={cagedChords}
renderOption={(chord) => `${capitalizeFirstLetter(chord)}`}
/>
)
}
To visualize the cyclical nature of the CAGED system, our template implementation displays six fretboards rather than just the five shapes. This arrangement demonstrates how the system wraps around the neck, with the sixth position showing the same chord as the first but as a barre chord on the 12th fret. This circular representation helps guitarists understand how these shapes connect across the entire fretboard, creating a continuous pattern that repeats at the octave.
import { VStack } from "@lib/ui/css/stack"
import { range } from "@lib/utils/array/range"
import { cagedChords } from "@product/core/chords/caged"
import { CagedTemplatePart } from "./CagedTemplatePart"
export const CagedTemplate = () => {
return (
<VStack gap={60}>
{range(cagedChords.length + 1).map((index) => (
<CagedTemplatePart key={index} index={index} />
))}
</VStack>
)
}
To determine which CAGED form to display at each position in our template sequence, we implement a utility function that calculates the appropriate form based on the selected root chord and position index. This function maintains the circular nature of the CAGED system by using modulo arithmetic to wrap around the chord array, ensuring a continuous sequence that properly represents how these shapes connect across the fretboard.
import { CagedChord, cagedChords } from "./caged"
export const getCagedTemplateForm = (chord: CagedChord, index: number) =>
cagedChords[(cagedChords.indexOf(chord) + index) % cagedChords.length]
To represent positions on the guitar fretboard, we use a simple data structure that captures both string and fret coordinates. Each note's location is identified by which string it's played on and at which fret position. The string is represented as a zero-based index (highest pitch string is 0) and the fret as a numeric value where -1 indicates an open string.
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
}
To complete our CAGED system implementation, we need two specialized chord collections to handle the unique requirements of visualizing these shapes across the fretboard. While most open chord shapes naturally extend into the CAGED sequence, the D chord requires special treatment because its standard open voicing typically spans only four strings. To achieve a complete six-string D major voicing that maintains consistent intervallic relationships when shifted up the neck as a barre chord, we extend the D shape in our cagedTemplateOpenChords
collection. In this extended version, we add the appropriate chord tones on the lower strings—fretting the low E string at the 2nd fret to produce F♯ (the major third) and using the open A string for the A (the fifth)—so that the complete D major chord (D, F♯, A) is represented. In contrast, our cagedTemplateBarreChords
collection omits the highest string because it can be difficult to fret cleanly when barred in higher positions, a practical adaptation that preserves the chord's harmonic integrity throughout the CAGED system.
import { NotePosition } from "../note/NotePosition"
export const cagedViews = ["chord", "arpeggio"] as const
export type CagedView = (typeof cagedViews)[number]
export const cagedChords = ["c", "a", "g", "e", "d"] as const
export type CagedChord = (typeof cagedChords)[number]
export const openCagedChords: Record<CagedChord, NotePosition[]> = {
c: [
{ string: 0, fret: -1 },
{ string: 1, fret: 0 },
{ string: 2, fret: -1 },
{ string: 3, fret: 1 },
{ string: 4, fret: 2 },
],
a: [
{ string: 0, fret: -1 },
{ string: 1, fret: 1 },
{ string: 2, fret: 1 },
{ string: 3, fret: 1 },
{ string: 4, fret: -1 },
],
g: [
{ string: 0, fret: 2 },
{ string: 1, fret: -1 },
{ string: 2, fret: -1 },
{ string: 3, fret: -1 },
{ string: 4, fret: 1 },
{ string: 5, fret: 2 },
],
e: [
{ string: 0, fret: -1 },
{ string: 1, fret: -1 },
{ string: 2, fret: 0 },
{ string: 3, fret: 1 },
{ string: 4, fret: 1 },
{ string: 5, fret: -1 },
],
d: [
{ string: 0, fret: 1 },
{ string: 1, fret: 2 },
{ string: 2, fret: 1 },
{ string: 3, fret: -1 },
],
}
export const cagedArpeggios: Record<CagedChord, NotePosition[]> = {
c: [
{ string: 0, fret: -1 },
{ string: 0, fret: 2 },
{ string: 1, fret: 0 },
{ string: 2, fret: -1 },
{ string: 3, fret: 1 },
{ string: 4, fret: 2 },
{ string: 5, fret: -1 },
{ string: 5, fret: 2 },
],
a: [
{ string: 0, fret: -1 },
{ string: 1, fret: 1 },
{ string: 2, fret: 1 },
{ string: 3, fret: -2 },
{ string: 3, fret: 1 },
{ string: 4, fret: -1 },
{ string: 5, fret: -1 },
],
g: [
{ string: 0, fret: 2 },
{ string: 1, fret: -1 },
{ string: 1, fret: 2 },
{ string: 2, fret: -1 },
{ string: 3, fret: -1 },
{ string: 4, fret: 1 },
{ string: 5, fret: 2 },
],
e: [
{ string: 0, fret: -1 },
{ string: 1, fret: -1 },
{ string: 2, fret: 0 },
{ string: 3, fret: 1 },
{ string: 4, fret: 1 },
{ string: 4, fret: -2 },
{ string: 5, fret: -1 },
],
d: [
{ string: 0, fret: 1 },
{ string: 1, fret: 2 },
{ string: 2, fret: 1 },
{ string: 2, fret: -2 },
{ string: 3, fret: -1 },
{ string: 4, fret: -1 },
{ string: 5, fret: 1 },
],
}
export const cagedPositions: Record<
CagedView,
Record<CagedChord, NotePosition[]>
> = {
chord: openCagedChords,
arpeggio: cagedArpeggios,
}
export const cagedTemplateDistances = [3, 2, 3, 2, 2]
export const cagedTemplateOpenChords: Record<CagedChord, NotePosition[]> = {
...openCagedChords,
d: [
{ string: 0, fret: 1 },
{ string: 1, fret: 2 },
{ string: 2, fret: 1 },
{ string: 3, fret: -1 },
{ string: 4, fret: -1 },
{ string: 5, fret: 1 },
],
}
export const cagedTemplateBarreChords: Record<CagedChord, NotePosition[]> = {
...cagedPositions.chord,
d: [
{ string: 1, fret: 2 },
{ string: 2, fret: 1 },
{ string: 3, fret: -1 },
{ string: 4, fret: -1 },
{ string: 5, fret: 1 },
],
}
To calculate the exact fretboard positions for each CAGED shape, we need a function that translates theoretical concepts into practical finger positions. The getCagedTemplatePartPositions
function serves this purpose by determining not only which chord form to use, but also where on the neck it should be positioned. The CAGED system follows specific intervallic distances between shapes—3 frets from C to A, 2 from A to G, 3 from G to E, and 2 from E to D—which we store in cagedTemplateDistances
. By rotating this array according to our starting chord and calculating cumulative distances, we can accurately position each shape at its proper fret position. This creates a continuous chain of the same chord ascending the neck, with each subsequent position shifted by the appropriate number of frets, maintaining harmonic consistency while changing fingering patterns.
import { rotateArray } from "@lib/utils/array/rotateArray"
import { sum } from "@lib/utils/array/sum"
import { match } from "@lib/utils/match"
import { NotePosition } from "../note/NotePosition"
import {
CagedChord,
CagedView,
cagedChords,
cagedPositions,
cagedTemplateBarreChords,
cagedTemplateDistances,
cagedTemplateOpenChords,
} from "./caged"
import { getCagedTemplateForm } from "./getCagedTemplateForm"
type GetCagedTemplatePartPositionsInput = {
chord: CagedChord
view: CagedView
index: number
}
export const getCagedTemplatePartPositions = ({
chord,
view,
index,
}: GetCagedTemplatePartPositionsInput): NotePosition[] => {
const form = getCagedTemplateForm(chord, index)
const distances = rotateArray(
cagedTemplateDistances,
cagedChords.indexOf(chord),
)
const openPositions = match(view, {
arpeggio: () => cagedPositions.arpeggio[form],
chord: () => {
const chords =
index === 0 ? cagedTemplateOpenChords : cagedTemplateBarreChords
return chords[form]
},
})
const shift = sum(distances.slice(0, index))
return openPositions.map((position) => ({
...position,
fret: position.fret + shift,
}))
}
When visualizing chord shapes across the fretboard, identifying the root note in its bass position provides crucial orientation for guitarists. The getChordPrimaryPosition
utility accomplishes this by locating the lowest-pitched occurrence of a specified note within each chord shape. By ordering positions from bass strings to treble strings and finding the first matching note, we ensure consistent root identification across all CAGED positions. This approach works universally for any chord in the CAGED system, helping guitarists maintain their bearings by highlighting the root note as it appears in different positions along the neck, regardless of which chord they're playing or which CAGED shape they're using.
import { order } from "@lib/utils/array/order"
import { tuning } from "../../app/guitar/config"
import { getNoteFromPosition } from "../note/getNoteFromPosition"
import { NotePosition } from "../note/NotePosition"
export const getChordPrimaryPosition = ({
positions,
note,
}: {
positions: NotePosition[]
note: number
}) =>
order(positions, (p) => p.string, "desc").find(
(position) => getNoteFromPosition({ tuning, position }) === note,
)
Now let's implement the CagedTemplatePart
component, the visual foundation of our CAGED system. This component integrates our utility functions to render each shape contextually on the fretboard. It determines the appropriate form for each position, calculates exact fret locations, and highlights the root note for orientation. For arpeggios, we use a special marker (fret value -2) to indicate notes that belong theoretically but can't be practically played in that position. The component filters these unplayable positions during rendering, keeping only open strings (-1) and fretted notes. Based on this filtering, it dynamically labels arpeggios as "incomplete" when appropriate—a situation that occurs with A, E, and D shapes in open positions. By the time we reach the 6th position (around the 12th fret), these arpeggios become fully playable. This final position completes our visualization and demonstrates how the CAGED system forms a continuous cycle repeating at the octave, providing clear insight into how these shapes interconnect across the entire fretboard.
import { VStack } from "@lib/ui/css/stack"
import { IndexProp } from "@lib/ui/props"
import { Text } from "@lib/ui/text"
import { match } from "@lib/utils/match"
import { getCagedTemplateForm } from "@product/core/chords/getCagedTemplateForm"
import { getCagedTemplatePartPositions } from "@product/core/chords/getCagedTemplatePartPositions"
import { getChordPrimaryPosition } from "@product/core/chords/getChordPrimaryPosition"
import { chromaticNotesNames } from "@product/core/note"
import { useMemo } from "react"
import { Fretboard } from "../../guitar/fretboard/Fretboard"
import { Note } from "../../guitar/fretboard/Note"
import { useCagedTemplate } from "./state/cagedTemplate"
export const CagedTemplatePart = ({ index }: IndexProp) => {
const { chord, view } = useCagedTemplate()
const form = getCagedTemplateForm(chord, index)
const positions = useMemo(() => {
return getCagedTemplatePartPositions({
chord,
view,
index,
})
}, [chord, index, view])
const primaryPosition = getChordPrimaryPosition({
positions,
note: chromaticNotesNames.indexOf(chord.toUpperCase()),
})
const title = useMemo(() => {
return match(view, {
arpeggio: () => {
const isIncomplete = positions.some((position) => position.fret < -1)
if (isIncomplete) {
return `Open ${chord.toUpperCase()} chord (incomplete arpeggio)`
}
return `${chord.toUpperCase()} arpeggio ("${form.toUpperCase()} form")`
},
chord: () =>
index === 0
? `Open ${chord.toUpperCase()} chord`
: `${chord.toUpperCase()} ("${form.toUpperCase()} form" barre chord)`,
})
}, [chord, form, index, positions, view])
return (
<VStack gap={40}>
<Text centerHorizontally color="contrast" as="h3" weight="700" size={18}>
{title}
</Text>
<Fretboard>
{positions
.filter((position) => position.fret >= -1)
.map((position) => (
<Note
key={`${position.string}-${position.fret}`}
string={position.string}
fret={position.fret}
kind={position === primaryPosition ? "primary" : "regular"}
/>
))}
</Fretboard>
</VStack>
)
}
By implementing this CAGED system visualization, we've created a practical tool that helps guitarists understand how chord shapes interconnect across the entire fretboard. This approach not only demystifies the relationship between chord forms but also provides a systematic framework for exploring different voicings of the same chord in multiple positions.