Implementing Arpeggios in the CAGED System for Guitar

Implementing Arpeggios in the CAGED System for Guitar

March 21, 2025

10 min read

Implementing Arpeggios in the CAGED System for Guitar

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.

5 CAGED Arpeggio shapes
5 CAGED Arpeggio shapes

Creating Type-Safe View Options

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]

Implementing Dynamic Routes

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

Managing State with React Context

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

Structuring the CAGED Page Component

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

Creating a View Switcher Component

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

Enhancing SEO with Dynamic Page Titles

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

Defining CAGED Chord Types

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]

Representing Notes on the Fretboard

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
}

Mapping Chord and Arpeggio Patterns

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

Rendering Chord and Arpeggio Shapes

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

Conclusion

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.