PluginUI

Piano Keyboard

piano-keyboardControls

One to ten octaves of real keys. Press a key to strike with velocity based on how far down the key you landed. The front of the key is the loud end, matching a real key's leverage and phone DAW convention. A floor keeps an edge tap quiet but never silent. Drag to glissando: the previous note turns off before the entered note turns on, so a monophonic host never hears an overlap. Notes leave through onNoteOn / onNoteOff, never through a param channel, because performance gestures are transient and must not enter saved state. A held map shaped like the module contract's activeNotes determines which keys light, so the keyboard reflects the audio rather than the pointer. It also handles declined notes: a struck key that made no sound appears struck and silent, not blank. Black keys use the equal-white-tops derivation instead of sitting at their boundaries, keeping scale runs even under the fingers. Placement and hit testing share arithmetic in a pure core. Scale membership and root tint are controlled props. One QWERTY input prop wires lib/computer-keyboard rather than rebuilding it. Every key is a real button with a note-name label; without callbacks, the keyboard becomes a read-only voicing display.

Bone / Studio
Loading interactive preview…
Planar / Flat
Loading interactive preview…
Midnight / Minimal
Loading interactive preview…
Ember / Vintage
Loading interactive preview…
Ink / Studio
Loading interactive preview…
Mint / Flat
Loading interactive preview…

Install

Copies the source into your project. Anything it depends on comes with it.

$ pnpm dlx shadcn@latest add @pluginui/core-piano-keyboard

Pulls in

keyboard-geometrycomputer-keyboarduse-surface-pointeruse-first-interactioncontrol-theme

First time? Add the registry to your components.json once:

components.json
{
  "registries": {
    "@pluginui": "https://pluginui.com/r/{name}.json"
  }
}

Usage

Connect the documented props to your application or host.

example.tsx
import { PianoKeyboard } from "@/components/ui/piano-keyboard"

// Playable. The host owns what is sounding and says so through `held`.
const [held, setHeld] = useState<Map<number, boolean>>(new Map())

<PianoKeyboard
  startNote={48}
  octaves={2}
  held={held}
  scalePcs={[0, 2, 4, 5, 7, 9, 11]}
  rootPc={0}
  computerKeyboard
  onNoteOn={(note, velocity) => {
    // velocity is 0..1, from how far down the key the press landed
    const sounded = synth.noteOn(note, velocity)
    setHeld((current) => new Map(current).set(note, sounded))
  }}
  onNoteOff={(note) => {
    synth.noteOff(note)
    setHeld((current) => {
      const next = new Map(current)
      next.delete(note)
      return next
    })
  }}
/>

// Read-only: no callbacks, so it draws a voicing rather than playing one.
<PianoKeyboard startNote={48} octaves={2} held={voicing} labels="c" />

Props

PropTypeDefaultDescription
startNote / octavesnumber48 / 2The range. The start snaps down to a white key (a black key has nothing to sit on otherwise) and the range includes its top C, so n octaves is 7n + 1 white keys and 12n + 1 in all.
onNoteOn(note, velocity) => void—Strike, with a 0..1 velocity from the press height. Passing either note callback makes the keyboard playable; passing neither makes it a read-only display.
onNoteOff(note) => void—Release. Called on pointer up, on leaving the surface, on the far side of a glissando step, and on Enter/Space key-up.
heldReadonlyMap<number, boolean>—MIDI notes currently held, mapped to whether each strike made a sound. This uses the module contract's activeNotes shape and meaning. True lights the key; false draws it struck but silent (dashed, dimmed and struck through) because an instrument must show when it declines a note. Absent means there is no information, not that nothing is held.
scalePcsnumber[]—Pitch classes in the current scale, tinted at the front of every octave of every member.
rootPcnumber—The scale's root, marked more strongly than the other members.
computerKeyboardbooleanfalseBind the QWERTY keyboard through lib/computer-keyboard. It provides physical-key mapping for AZERTY and Dvorak, a text-field guard, held-key sustain and spacebar panic. Chord degrees are disabled because a keyboard has no chord bank. The library does not bind octave-shift keys, so − / = shift the octave here. Ignored when read-only.
computerKeyboardLayout"daw-piano-qwerty" | "chromatic-qwerty""daw-piano-qwerty"Which of that lib's mappings to bind.
labels"none" | "c" | "all""c"Which keys carry a name. Labelling only the Cs is what a player navigates by; every other label is noise once you know where C is.
velocitynumber0.8Velocity for strikes without a press position, including Enter/Space and computer-keyboard input. It matches lib/computer-keyboard's default so typed and clicked notes sound alike.
onInteraction(event) => void—Fires once on the first gesture of any kind. Use it to resume a suspended AudioContext.
colorstring—Override the accent colour for this instance.
heightnumber120Keyboard height in pixels; width defaults to 100%.

Interaction

  • Drag with pointer capture, so the gesture keeps tracking past the edge of the control.
  • Shift while dragging for a fine pass; release it mid-gesture and coarse tracking resumes.
  • Double-click resets to defaultValue.
  • Arrow keys step normally, Shift+arrow steps finely, PageUp/PageDown jump by ten, Home/End go to the extremes.
  • Proper slider and switch roles with live aria-valuetext, so a screen reader announces the formatted value rather than the raw number.
  • The scroll wheel adjusts the value while the pointer is over the control; stepped ranges move at least one declared step per notch by default.