This is the fifth post in our series building a React guitar theory app. So far, we've created a fretboard with various scales and implemented the CAGED system's five basic open chords. Now we're adding arpeggios to our CAGED page—a natural progression since arpeggios break down chords into individual notes played sequentially, helping guitarists understand chord construction while developing technique for melodic playing. You can find all the source code in the GitHub repository.
To implement switching between chord and arpeggio views, we'll define a type-safe union of the two views using TypeScript:
export const cagedViews = ["chord", "arpeggio"] as const
export type CagedView = (typeof cagedViews)[number]
Next, we'll leverage Next.js dynamic routes to create our CAGED page with switchable views. This pattern generates separate static pages for chord and arpeggio views at build time, improving performance. We'll use getStaticPaths
to generate routes from our cagedViews
array, while GetStaticProps
passes the current view to our CagedPage
component, enabling view-specific rendering.
import { GetStaticPaths, GetStaticProps } from "next"
import { CagedPage } from "../../../caged/CagedPage"
import { cagedViews } from "@product/core/chords/caged"
import { CagedState } from "../../../caged/state/caged"
export default CagedPage
export const getStaticPaths: GetStaticPaths<CagedState> = async () => {
const paths = cagedViews.map((view) => ({
params: {
view,
},
}))
return {
paths,
fallback: false,
}
}
export const getStaticProps: GetStaticProps = async ({ params }) => {
const { view } = params as CagedState
return {
props: {
value: {
view,
},
},
}
}
To manage state for our CAGED component, we'll implement a React context for tracking the current view. The getValueProviderSetup
utility from RadzionKit is a small helper that simplifies creating single value providers by handling the context creation, provider component, and hook generation in one concise function. The resulting CagedProvider
wraps our page component to make the selected view available to all children without prop drilling. Additionally, we create two utilities: makeCagedPath
generates URL paths based on state, while useChangeCaged
provides a convenient hook for navigating between views by updating the URL.
import { getValueProviderSetup } from "@lib/ui/state/getValueProviderSetup"
import { useRouter } from "next/router"
import { useCallback } from "react"
import { CagedView } from "@product/core/chords/caged"
export type CagedState = {
view: CagedView
}
export const makeCagedPath = ({ view }: CagedState) => `/caged/${view}`
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],
)
}
Our page structure uses a CagedProvider
to wrap the entire CAGED page, creating a shared context for view state. The page layout consists of three main components: a ManageCagedView
component for switching between chord and arpeggio views, a CagedPageTitle
, and a responsive grid of CagedItem
components displayed in a horizontally stacked layout.
import { HStack, VStack } from "@lib/ui/css/stack"
import { PageContainer } from "../layout/PageContainer"
import { CagedPageTitle } from "./CagedPageTitle"
import { cagedChords } from "@product/core/chords/caged"
import { CagedProvider, CagedState } from "./state/caged"
import { ValueProp } from "@lib/ui/props"
import { CagedItem } from "./CagedItem"
import { ManageCagedView } from "./manage/ManageCagedView"
export const CagedPage = ({ value }: ValueProp<CagedState>) => (
<CagedProvider value={value}>
<PageContainer>
<VStack gap={60}>
<VStack alignItems="center">
<ManageCagedView />
</VStack>
<CagedPageTitle />
<HStack
fullWidth
gap={60}
alignItems="center"
justifyContent="center"
wrap="wrap"
>
{cagedChords.map((chord) => (
<CagedItem key={chord} value={chord} />
))}
</HStack>
</VStack>
</PageContainer>
</CagedProvider>
)
For our view switching interface, we implement a ManageCagedView
component that leverages the GroupedRadioInput
component from RadzionKit to create an intuitive toggle between chord and arpeggio views.
import { GroupedRadioInput } from "@lib/ui/inputs/GroupedRadioInput"
import { useChangeCaged, useCaged } from "../state/caged"
import { cagedViews } from "@product/core/chords/caged"
import { capitalizeFirstLetter } from "@lib/utils/capitalizeFirstLetter"
export const ManageCagedView = () => {
const { view } = useCaged()
const setValue = useChangeCaged()
return (
<GroupedRadioInput
value={view}
onChange={(view) => setValue({ view })}
options={cagedViews}
renderOption={(view) => `${capitalizeFirstLetter(view)}s`}
/>
)
}
To ensure consistent SEO across our application, we'll create a CagedPageTitle component that dynamically generates page titles and descriptions based on the current view. This component pulls the view state from our context, capitalizes the view name, and uses the match
utility for view-specific description text.
import { VStack } from "@lib/ui/css/stack"
import { useCaged } from "./state/caged"
import { PageMetaTags } from "@lib/next-ui/metadata/PageMetaTags"
import { capitalizeFirstLetter } from "@lib/utils/capitalizeFirstLetter"
import { match } from "@lib/utils/match"
import { PageTitle } from "../ui/PageTitle"
export const CagedPageTitle = () => {
const { view } = useCaged()
const viewTitle = `${capitalizeFirstLetter(view)}s`
const title = `CAGED ${viewTitle} System for Guitar | Master Fretboard Positions`
const description = `Interactive guide to the CAGED ${viewTitle.toLowerCase()} system for guitarists. Learn C, A, G, E, D ${match(
view,
{
chord: () => "chord shapes",
arpeggio: () => "arpeggio patterns",
},
)} to navigate the entire fretboard and improve your playing.`
return (
<VStack>
<PageMetaTags title={title} description={description} />
<PageTitle>CAGED {viewTitle}</PageTitle>
</VStack>
)
}
To render each chord or arpeggio shape, we map through the cagedChords
array, rendering a CagedItem
component for each chord position.
export const cagedChords = ["c", "a", "g", "e", "d"] as const
export type CagedChord = (typeof cagedChords)[number]
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
}
We'll use the openCagedChords
and cagedArpeggios
objects to store the note positions for each chord and arpeggio shape. The C and G arpeggios can be played in the same position as their corresponding open chords, maintaining their familiar low-position fingerings. However, the A, E, and D arpeggios contain notes that would extend beyond the guitar's physical limitations if played in open position. To address this, we shift these arpeggios one octave higher (same notes, but 12 frets higher in pitch), placing them around the 11th-13th frets where all notes can be comfortably reached within a single hand position.
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: 11 },
{ string: 1, fret: 13 },
{ string: 2, fret: 13 },
{ string: 3, fret: 10 },
{ string: 3, fret: 13 },
{ string: 4, fret: 11 },
{ string: 5, fret: 11 },
],
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: 11 },
{ string: 1, fret: 11 },
{ string: 2, fret: 12 },
{ string: 3, fret: 13 },
{ string: 4, fret: 13 },
{ string: 4, fret: 10 },
{ string: 5, fret: 11 },
],
d: [
{ string: 0, fret: 13 },
{ string: 1, fret: 14 },
{ string: 2, fret: 13 },
{ string: 2, fret: 10 },
{ string: 3, fret: 11 },
{ string: 4, fret: 11 },
{ string: 5, fret: 13 },
],
}
Now let's examine how the CagedItem
component renders each chord or arpeggio shape with proper highlighting of the root note. For better visual identification, we need to distinguish the root note from other chord tones. The component calculates the lowestBassString
by filtering all positions to find notes that match the chord's root note value, then determining which of these is on the lowest bass string (highest string number in our zero-based index). This root note is then rendered with the "primary" styling to provide consistent visual anchoring across both chord and arpeggio shapes.
The actual rendering of notes on the fretboard uses our custom Fretboard
and Note
components, which handle the visual representation of string positions, fret markers, and note indicators. The component also intelligently calculates the visible fret range to ensure all notes are displayed clearly, with a minimum of 4 frets shown. For a deeper understanding of how these fretboard visualization components work, refer to the first post in this series where we established the foundational rendering system.
import { ValueProp } from "@lib/ui/props"
import {
cagedArpeggios,
CagedChord,
openCagedChords,
} from "@product/core/chords/caged"
import { vStack } from "@lib/ui/css/stack"
import { Text } from "@lib/ui/text"
import { Fretboard } from "../guitar/fretboard/Fretboard"
import { Note } from "../guitar/fretboard/Note"
import styled from "styled-components"
import { useCaged } from "./state/caged"
import { getNoteFromPosition } from "@product/core/note/getNoteFromPosition"
import { tuning } from "../guitar/config"
import { chromaticNotesNames } from "@product/core/note"
const Container = styled.div`
${vStack({
gap: 40,
})}
min-width: 320px;
max-width: 400px;
`
const positionsRecord = {
chord: openCagedChords,
arpeggio: cagedArpeggios,
}
const minVisibleFrets = 4
export const CagedItem = ({ value }: ValueProp<CagedChord>) => {
const { view } = useCaged()
const positions = positionsRecord[view][value]
const lowestBassString = Math.max(
...positions
.filter(
(position) =>
getNoteFromPosition({ tuning, position }) ===
chromaticNotesNames.indexOf(value.toUpperCase()),
)
.map((position) => position.string),
)
const firstVisibleFret = Math.min(
...positions.map((position) => position.fret),
)
return (
<Container>
<Text centerHorizontally color="contrast" as="h3" weight="700" size={18}>
{value.toUpperCase()} major {view}
</Text>
<Fretboard
visibleFrets={{
start: firstVisibleFret,
end: Math.max(
...positions.map((position) => position.fret),
firstVisibleFret + minVisibleFrets,
),
}}
>
{positions.map((position) => (
<Note
key={`${position.string}-${position.fret}`}
string={position.string}
fret={position.fret}
kind={position.string === lowestBassString ? "primary" : "regular"}
/>
))}
</Fretboard>
</Container>
)
}
With both chord shapes and arpeggios implemented in our CAGED system, guitarists can now switch between viewing full chord voicings and their broken-down note patterns. This provides a comprehensive learning tool that helps players understand chord construction while developing the technical facility to incorporate these patterns into their solos and improvisation.