A note editor on a grid: draw, move, resize and select notes, with a playable keyboard and a velocity lane.
Base UI
A piano roll for writing a phrase: a keyboard down the side, notes on a grid, and a velocity bar under each note. It sits inside a Timeline, so notes use the timeline's units, snap to its grid and follow its zoom.
<input type="range">.Import the registry components for the ready-made look, or compose the primitive's parts yourself.
import { PianoRoll, Timeline } from "@audio-ui/react";
<Timeline.Root duration={16} snapStep={1}>
<PianoRoll.Root>
<PianoRoll.Keys>
<PianoRoll.Rows>
{(pitch) => (
<PianoRoll.Key pitch={pitch}>
<PianoRoll.KeyLabel />
</PianoRoll.Key>
)}
</PianoRoll.Rows>
</PianoRoll.Keys>
<PianoRoll.Grid aria-label="Notes">
<PianoRoll.Rows>{(pitch) => <PianoRoll.Row pitch={pitch} />}</PianoRoll.Rows>
<PianoRoll.Notes>
{(note) => (
<PianoRoll.Note key={note.id} note={note}>
<PianoRoll.NoteHandle edge="start" />
<PianoRoll.NoteHandle edge="end" />
</PianoRoll.Note>
)}
</PianoRoll.Notes>
<PianoRoll.Marquee />
</PianoRoll.Grid>
<PianoRoll.VelocityLane aria-label="Velocity">
<PianoRoll.Notes>
{(note) => <PianoRoll.Velocity key={note.id} note={note} />}
</PianoRoll.Notes>
</PianoRoll.VelocityLane>
</PianoRoll.Root>
</Timeline.Root>;import { Timeline, TimelineNavigator } from "@/components/audio/elements/timeline";
import {
PianoRoll,
PianoRollGrid,
PianoRollKeys,
PianoRollStrip,
PianoRollVelocity,
PianoRollVelocityScale,
PianoRollViewport,
} from "@/components/audio/elements/piano-roll";
<Timeline duration={16} snapStep={1}>
<PianoRoll
className="h-96"
maxPitch={72}
minPitch={48}
onNoteOn={(pitch, details) => play(pitch, details.velocity)}
onValueChange={setNotes}
value={notes}
>
<PianoRollViewport>
<PianoRollStrip className="flex-1" lead={<PianoRollKeys />}>
<PianoRollGrid aria-label="Notes" gridStep={1} />
</PianoRollStrip>
<PianoRollStrip lead={<PianoRollVelocityScale />} sticky="bottom">
<PianoRollVelocity />
</PianoRollStrip>
</PianoRollViewport>
</PianoRoll>
</Timeline>;A note is { id, pitch, start, length, velocity? }: pitch is a MIDI number (60 is middle C), start and length are in the timeline's units, and velocity is 0 to 1 (full when left out). value and selection are controlled; defaultValue and defaultSelection leave them to the roll. The roll reports every edit through onValueChange and, when a gesture or a key press ends, onValueCommitted; eventDetails.edit tells what happened ("add", "move", "resize", "remove", "duplicate", "paste", "quantize" or "velocity") and eventDetails.cancel() keeps the notes as they were.
onNoteOn and onNoteOff say when a pitch should sound or stop: a key pressed, a note drawn, picked up or moved to a new pitch. A MIDI file can be read and written with the midi library (@audio/midi); the preview above imports and exports .mid files with it.
Gestures · 8
mode="draw", draws a note whose end follows the pointer. With mode="select", starts a selection rectangle (a double-click draws). Shift adds to the selection.Shift, selects every note on its row.Keys · 9
Left / Right move it by a grid step, Up / Down by a semitone. With Shift: resize it, or move an octave.Groups the parts of a piano roll and owns its notes and selection. Must be used within a Timeline. Renders a <div> element.
Props · 18
"note-<n>", skipping ids in use.eventDetails.velocity is 0 to 1; eventDetails.cancel() keeps it silent.Data attributes · 4
data-disableddata-editingdata-foldeddata-mode"draw" or "select".The keyboard down the side: a Piano laid out on the roll's rows, low end at the bottom. Pressing a key sounds it through the roll's onNoteOn; with Shift, pressing it (or Shift + Enter) selects every note on its row, or deselects them when they all are. Put a PianoRoll.Key per row inside, with PianoRoll.Rows. Renders a <div> element.
Renders children once per row, from the highest pitch down: the place to put a PianoRoll.Key or a PianoRoll.Row. Renders no element of its own.
Props · 1
The same parts as Piano: a key of the keyboard, and its label.
The surface notes are drawn on: a timeline lane, with the roll's rows stacked down it. A press on empty grid draws a note (mode="draw"), whose end then follows the pointer, or starts a selection rectangle, Shift adding to what is selected; in that mode a double-click draws. Focused, Enter adds a note at the playback position, Escape clears the selection and Cmd/Ctrl+A selects every note. From the grid or a note, Cmd/Ctrl+D repeats the selection right after itself, Cmd/Ctrl+C, X and V copy, cut and paste at the playback position, and Q snaps the starts of the selection (every note, with none selected) to the grid. Renders a <div> element.
The band a pitch takes across the grid, for shading black-key rows or ruling where an octave starts. It lets presses through to the grid and is hidden from assistive technology. Renders a <div> element.
Props · 1
Data attributes · 4
data-activedata-color"black" or "white".data-foldeddata-octaveRenders children once per note, in the order they are read: by start, then from the highest pitch down. That order is also the tab order. Renders no element of its own.
Props · 1
One note, placed on its row from its start, as long as its length. A toggle button named after its pitch and span (getNoteLabel on Root). Renders a <div> element.
Props · 1
Data attributes · 2
data-selectedaria-pressed.data-disabledOne edge of the note it sits in, dragged to change the note's length: the end makes it longer or shorter, the start trims it from the front while its end stays put. On the grid unless Cmd/Ctrl is held, and for the whole selection when the note is part of it. Shift + Left and Right on the note do the same from the keyboard, so the handle is hidden from assistive technology. Renders a <div> element.
Props · 1
The selection rectangle being dragged across the grid; renders nothing the rest of the time. Hidden from assistive technology. Renders a <div> element.
A strip under the grid where each note's velocity stands as a bar, placed at the note's start: a timeline lane, like the grid. Put a PianoRoll.Velocity per note inside, with PianoRoll.Notes. Renders a <div> element.
One note's velocity, as a bar from the bottom of the lane: its height is the velocity, through the --piano-roll-velocity CSS variable. Dragging it up or down changes the velocity of the selection by the same amount when the note is part of it, so a phrase keeps its dynamics; Shift + click toggles the note in the selection. A native <input type="range"> inside takes the arrow keys and carries the accessible name, which defaults to the note's. The next drawn note gets the velocity set last. Renders a <div> element.
Props · 2
PianoRoll takes the Root props. The rest lay the editor out on two columns, the keys' and the timeline's:
Components · 6
PianoRollViewportTimelineNavigator above it to scroll and zoom across.PianoRollStriplead in the keys' column, stuck to the left edge, the lanes in children beside it. sticky keeps it at the "top" or "bottom" of the viewport.PianoRollKeyssize is "sm", "default" or "lg".PianoRollGridgridStep draws a line every step, accentStep a firmer one, such as each beat; anything placed on the timeline, such as a TimelinePlayhead, goes in children.PianoRollVelocityPianoRollVelocityScalelead of its strip.