Skip to main content

Editing scores

react-notation components are interactive by default — but only when you opt in. Pass a ScoreEditor (from useScore) to any rendering component to activate editing mode.

Quick start

import { ChordSheet, useScore, createScore, createTrack, createMeasure, createChord } from 'react-notation'
import 'react-notation/style.css'

const initialScore = createScore({
timeSignature: { beats: 4, value: 4 },
keySignature: { root: 'C', mode: 'major' },
tracks: [
createTrack({
measures: [
createMeasure({ number: 1, events: [
createChord({ beat: 1, duration: 'half', symbol: 'C', root: 'C' }),
createChord({ beat: 3, duration: 'half', symbol: 'Am', root: 'A', quality: 'minor' }),
]}),
],
}),
],
})

function Editor() {
const editor = useScore(initialScore)

return (
<ChordSheet
score={editor.score}
editor={editor}
/>
)
}

That's it. Every chord symbol is now:

  • Focusable via Tab
  • Editable with Enter (shows an inline input)
  • Removable with Delete
  • Navigable with arrow keys

useScore API

const editor = useScore(initialScore)
PropertyTypeDescription
scoreMusicScoreCurrent score state
selectionSelection | nullCurrently selected element
select(id)(id: string | null) => voidProgrammatically move selection
updateChord(id, patch)Update a chord's fields
updateLyric(id, patch)Update a lyric's fields
updateNote(id, patch)Update a note's fields
updateMeasure(id, patch)Update a measure's fields
addEvent(measureId, event)Append an event to a measure
removeEvent(id)Remove an event
moveEvent(id, toBeat)Move an event to a different beat
addMeasure(trackId, after?)Insert a new measure
removeMeasure(id)Remove a measure (renumbers remaining)
addTrack(after?)Insert a new track
removeTrack(id)Remove a track
undo()Undo last mutation
redo()Redo last undone mutation
canUndobooleanWhether undo is available
canRedobooleanWhether redo is available

Keyboard navigation (ChordSheet)

KeyAction
Tab / Shift+TabMove between selectable elements
/ Previous / next beat
/ Same beat on previous / next line
EnterBegin inline editing of a chord symbol
Any printable charBegin editing, seeded with the typed character
EscapeCancel edit
Delete / BackspaceRemove selected event

Attaching custom popups

Every interactive element receives data-notation-id and data-notation-type attributes, making it easy to anchor a popup to the focused element without any special library APIs:

function ChordEditor() {
const editor = useScore(initialScore)
const [anchor, setAnchor] = React.useState<HTMLElement | null>(null)

return (
<>
<ChordSheet
score={editor.score}
editor={editor}
inlineEdit={false} // suppress built-in input
onEditStart={(selection) => {
const el = document.querySelector(`[data-notation-id="${selection.id}"]`)
setAnchor(el as HTMLElement)
}}
onEditCancel={() => setAnchor(null)}
/>
{anchor && (
<MyChordPicker
anchor={anchor}
onSelect={(symbol, root, quality) => {
if (editor.selection) {
editor.updateChord(editor.selection.id, { symbol, root, quality })
}
setAnchor(null)
}}
/>
)}
</>
)
}

Editor callbacks

PropSignatureWhen it fires
onSelect(sel: Selection) => voidElement focused or clicked
onEditStart(sel: Selection) => void | falseEditing begins. Return false to suppress the built-in input.
onEditCommit(sel: Selection, value: string) => voidEdit confirmed (Enter or blur). Editor has already been updated.
onEditCancel(sel: Selection) => voidEdit cancelled (Escape).

Persisting changes

useScore is uncontrolled — it owns its state internally. To persist changes, read editor.score whenever you need the current state:

function AutoSave() {
const editor = useScore(initialScore)

React.useEffect(() => {
localStorage.setItem('my-score', JSON.stringify(editor.score))
}, [editor.score])

return <ChordSheet score={editor.score} editor={editor} />
}

Undo / redo

useScore maintains a full history stack. Every mutation is a new entry; undo/redo are instant (no re-parsing).

function EditorWithHistory() {
const editor = useScore(initialScore)

return (
<div>
<button onClick={editor.undo} disabled={!editor.canUndo}>Undo</button>
<button onClick={editor.redo} disabled={!editor.canRedo}>Redo</button>
<ChordSheet score={editor.score} editor={editor} />
</div>
)
}

Selection model

interface Selection {
type: 'chord' | 'lyric' | 'note' | 'rest' | 'measure' | 'beat'
id: string // event id (or measure id for type 'measure')
measureId: string
trackId: string
beat?: number
}

Use editor.selection to read the current selection, or editor.select(id) to set it programmatically (e.g. to focus the first chord on mount).