Files
pocket-chords/docs/superpowers/specs/2026-04-09-chord-diagram-design.md

5.7 KiB

Chord Diagram Feature Design

Date: 2026-04-09

Overview

A chord diagram feature for the song detail page that shows users how to play each chord on piano or guitar. The core component is dumb — it receives only a chord name string and renders the diagram. All music theory and voicing logic lives in a separate library layer.

Architecture

Three cleanly separated layers:

chord name string ("Cmaj7")
        │
        ▼
  [theory layer]  — tonal parses name → root + note set {C, E, G, B}
        │
        ▼
  [voicing layer] — maps note set → renderable positions
      ├── Piano: note set → highlight keys on 1-octave keyboard
      └── Guitar: chord quality + root → transpose moveable shape template
        │
        ▼
  [render layer]  — dumb components, no music theory knowledge
      ├── <PianoKeys notes={["C","E","G","B"]} />
      └── <GuitarFretboard frets={[x,3,2,0,1,0]} baseFret={1} />

Files

app/app/
  lib/
    chord-voicing.ts        # theory layer: tonal → notes + guitar voicing
    guitar-voicings.ts      # data: ~25 quality templates
  components/
    chord-diagram/
      piano-keys.tsx        # dumb renderer: string[] of note names → keyboard
      guitar-fretboard.tsx  # dumb renderer: frets[] + baseFret → fretboard grid
      chord-diagram.tsx     # entry point: chord+instrument → voicing → renderer
      chord-grid.tsx        # wrapped grid of ChordDiagram cards for all song chords

Component API

<ChordDiagram>

<ChordDiagram chord="Cmaj7" instrument="piano" />
<ChordDiagram chord="Am" instrument="guitar" />

Renders nothing (graceful empty) if the chord cannot be parsed or has no voicing.

<ChordGrid>

<ChordGrid chords={["Em7", "G", "Dsus4", "Am7"]} />

Owns the instrument state ("piano" | "guitar"), persisted to localStorage as chordDiagramInstrument. Renders a global piano/guitar toggle and a flex-wrap grid of <ChordDiagram> cards.

Diagram Styles

  • Piano: dot notation — white keys with filled circles on pressed keys, black keys overlaid. 1 fixed octave shown (C to B); notes are matched by name regardless of octave.
  • Guitar: standard vertical fretboard — nut at top, 4 frets shown, dots on finger positions, O/X above strings for open/muted. Barre indicator where applicable.

Theory Layer (chord-voicing.ts)

Uses @tonaljs/tonal (already in npm, tree-shakeable):

export function getPianoNotes(chord: string): string[]
// "Cmaj7" → ["C", "E", "G", "B"]
// Returns [] if unparseable

export function getGuitarVoicing(chord: string): GuitarVoicing | null
// "Am" → { frets: [0,0,2,2,1,0], baseFret: 1, barre: null }
// Returns null if quality not in voicing map

Guitar Voicing Data (guitar-voicings.ts)

~25 moveable barre-chord templates keyed by tonal chord type name. Each template is a barre shape (no open strings) so it can be transposed by shifting baseFret. Two shape families are used: E-shapes (root on 6th string) and A-shapes (root on 5th string). baseFret is computed as the semitone distance from the template shape's root string pitch (E or A) to the target chord root.

interface GuitarVoicingTemplate {
  frets: (number | null)[]  // 6 strings; null = muted; fret numbers relative to baseFret
  baseFret: number          // 1 in template; shifted when transposing to target root
  barre: number | null      // fret (relative to baseFret) to draw barre, or null
  rootString: 'E' | 'A'    // which string carries the root (determines transposition offset)
}

Quality names match tonal's Chord.get(name).type output (e.g. "major", "minor", "major seventh", "dominant seventh", "minor seventh", "diminished", "augmented", "suspended fourth", "suspended second", "half-diminished", "dominant seventh flat five", etc.). ~25 entries total.

If tonal returns a quality name not in the map, getGuitarVoicing returns null and the diagram renders a "no guitar voicing" placeholder.

Layout & Integration

Breakpoint

lg (Tailwind) divides mobile from desktop layout.

Mobile

  • Below lg: lyrics and diagrams in a single column.
  • Inline popup: tapping a chord name in chord-chart.tsx sets activeChord state in songs.$id.tsx. An inline <ChordDiagram> panel appears immediately below the tapped line. It closes when the scroll container fires a scroll event.
  • Bottom grid: <ChordGrid> rendered after <ChordChart> in the scroll column. Not sticky — scrolls with content.

Desktop

  • At lg and above: songs.$id.tsx switches to a two-column layout.
  • Left column: <ChordChart> (existing).
  • Right column: <ChordGrid> showing all unique chords in the song, wrapped. No inline popup on desktop (side column is always visible).

Chord list source

songs.$id.tsx derives uniqueChords: string[] from displayed.sections — all unique chord names in order of first appearance, deduplicated. This list is passed to <ChordGrid> and also used to determine which chord names in <ChordChart> are tappable.

Instrument toggle

Global piano/guitar toggle lives in <ChordGrid>. State persisted to localStorage as chordDiagramInstrument. Switching updates all visible diagrams at once.

Error / Unknown Chord Handling

  • getPianoNotes returns []<PianoKeys> renders with no dots highlighted and a subtle "?" label.
  • getGuitarVoicing returns null<GuitarFretboard> renders an empty fretboard with a "no voicing" label.
  • Unparseable chord name (garbage string) → same fallback as above.

Dependency

Add tonal to app/package.json. It is tree-shakeable; only chord parsing and note utilities will be bundled.