# Piano

> A piano keyboard for a MIDI pitch range, played with the pointer, touch or the keyboard.

```tsx
"use client";

import { Piano } from "@/components/audio-ui/piano";
import { useMidiInput } from "@/hooks/use-midi-input";

/** A keyboard from C3 to C5. With a MIDI keyboard connected, the keys you hold on it light up here. */
export function PianoSizeDefaultDemo() {
  const midi = useMidiInput();
  return (
    <Piano
      activeNotes={midi.activeNotes}
      aria-label="Piano"
      maxPitch={72}
      minPitch={48}
    />
  );
}
```

Plays or shows notes, for a virtual instrument, a MIDI visualizer or a chord reference. The registry component wraps the `Piano` primitive from `@audio-ui/react` and draws white and black keys for any MIDI range.

## Overview

- **Range:** `minPitch` and `maxPitch` are MIDI pitches (default `48` and `72`, C3 to C5), or `pitches` lists the exact keys to show. Key colors and layout follow the chromatic pattern.
- **Events:** `onNoteOn(pitch, eventDetails)` and `onNoteOff(pitch, eventDetails)` report MIDI pitches, ready for an audio engine. `eventDetails.reason` says how the note started (`"press"`, `"glide"` or `"keyboard"`), and `eventDetails.cancel()` in `onNoteOn` keeps the key silent.
- **Pointer:** press and hold a key to hold its note. Drag across keys for a glissando: each key plays as the pointer crosses it. Each touch holds its own key, so chords work on a touch screen.
- **External notes:** `activeNotes` lights keys from outside, such as a MIDI input or a sequencer's current chord, without calling `onNoteOn` or `onNoteOff`.
- **Accessibility:** each key is a button named after its pitch ("C4"), behind one tab stop. The arrow keys move between keys, and Space or Enter holds the focused key until it is released.
- **Layout:** the primitive can also lay keys out vertically (`orientation="vertical"`, lowest key at the bottom) or in equal widths (`layout="even"`). The piano roll's keyboard uses this.

## Installation

**CLI**

```bash
npx shadcn@latest add @audio-ui/piano
```

**Manual**

**1. Install the following dependencies:**

```bash
npm install @audio-ui/react class-variance-authority
```

**2. Copy and paste the following code into your project.**

```tsx title="components/audio-ui/piano.tsx"
"use client";

import { Piano as PianoPrimitive } from "@audio-ui/react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";

const pianoVariants = cva("", {
  defaultVariants: {
    size: "default",
  },
  variants: {
    size: {
      default: "h-32",
      lg: "h-40",
      sm: "h-24",
    },
  },
});

interface PianoProps
  extends Omit<PianoPrimitive.Root.Props, "className">,
    VariantProps<typeof pianoVariants> {
  className?: string;
}

export function Piano({ className, size, ...props }: PianoProps) {
  return (
    <PianoPrimitive.Root
      className={cn(
        "rounded-xl border border-transparent bg-card ring-1 ring-foreground/10 transition-[color,border-color,box-shadow,opacity] duration-150 ease-out hover:ring-3 hover:ring-ring/50 has-focus-visible:ring-3 has-focus-visible:ring-ring/50 active:ring-3 active:ring-ring/50 has-focus-visible:border-ring active:border-ring motion-reduce:transition-none [--piano-key-white:oklch(0.985_0_0)] [--piano-key-black:oklch(0.205_0_0)] w-full touch-none select-none overflow-hidden",
        "data-disabled:pointer-events-none data-disabled:opacity-50",
        pianoVariants({ size }),
        className
      )}
      data-slot="piano"
      {...props}
    >
      <PianoPrimitive.Keys>
        {(pitch) => (
          <PianoPrimitive.Key
            className="top-0 outline-none transition-colors duration-100 ease-out focus-visible:z-20 focus-visible:ring-2 focus-visible:ring-ring/60 focus-visible:ring-inset data-[color=white]:z-0 data-[color=white]:h-full data-[color=white]:border-border data-[color=white]:border-x data-[color=white]:bg-(--piano-key-white) data-[color=black]:z-10 data-[color=black]:h-[60%] data-[color=black]:rounded-b-[min(var(--radius-sm),4px)] data-[color=black]:bg-(--piano-key-black) data-[active]:data-[color]:bg-primary motion-reduce:transition-none cursor-pointer"
            key={pitch}
            pitch={pitch}
          />
        )}
      </PianoPrimitive.Keys>
    </PianoPrimitive.Root>
  );
}
```

**3. Update the import paths to match your project setup.**

## Anatomy

Import the primitive and assemble its parts.

```tsx
import { Piano } from "@audio-ui/react";

<Piano.Root>
  <Piano.Keys>
    {(pitch) => (
      <Piano.Key pitch={pitch}>
        <Piano.KeyLabel />
      </Piano.Key>
    )}
  </Piano.Keys>
</Piano.Root>
```

## Usage

```tsx
import { Piano } from "@/components/audio-ui/piano";
```

**Note events:**

```tsx
<Piano
  maxPitch={72}
  minPitch={48}
  onNoteOff={(pitch) => synth.release(pitch)}
  onNoteOn={(pitch) => synth.trigger(pitch)}
/>
```

**External notes (MIDI input, a sequencer's current chord):**

```tsx
<Piano activeNotes={midiInputNotes} maxPitch={72} minPitch={48} />
```

**A MIDI keyboard:** `useMidiInput` listens to the page's MIDI input. Browsers ask for permission, so connect it from a click: with `requestAccess`, or the ready-made button, `npx shadcn@latest add @audio-ui/midi-connect-control`.

```tsx
import { useMidiInput } from "@/hooks/use-midi-input";

const midi = useMidiInput({
  onNoteOff: (pitch) => synth.release(pitch),
  onNoteOn: (pitch, velocity) => synth.trigger(pitch, velocity),
});

<button onClick={midi.requestAccess} type="button">Connect MIDI</button>
<Piano activeNotes={midi.activeNotes} maxPitch={72} minPitch={48} />
```

Everything `useMidiInput` gives is in [Input](/docs/engine/input).

## Interaction

### Pointer

**Behavior**

| Gesture | Effect |
| --- | --- |
| Press a key | Fires `onNoteOn`; the key stays pressed while held. |
| Release | Fires `onNoteOff`. |
| Drag across keys | Glissando: the previous key releases (`onNoteOff`) and the next one sounds (`onNoteOn`) as the pointer crosses it. |
| Multiple pointers | Each pointer (finger) holds its own key, so chords work on touch devices. |

### Keyboard

The keys share one tab stop. On the focused key:

**Keys**

| Key | Effect |
| --- | --- |
| `ArrowLeft` / `ArrowRight` (or `ArrowDown` / `ArrowUp`) | Move focus to the next lower / higher key |
| `Shift` + arrow, `PageDown` / `PageUp` | Move focus an octave down / up |
| `Space` / `Enter` | Hold the focused key: `onNoteOn` on press, `onNoteOff` on release |
| `Home` / `End` | Move focus to the lowest / highest key |

## API reference

### Root

A keyboard to play: every key sounds while pressed, by pointer or from the keyboard. Dragging across the keys glides from one to the next, and each pointer holds its own key, so chords can be played on a touch screen. It is laid out like a piano, or with `layout="even"` as one equal share per key, the way a piano roll's rows are; `orientation="vertical"` puts the low end at the bottom. The keys are one tab stop, walked with the arrow keys. Renders a `<div>` element.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `activeNotes` | `readonly number[]` |  | Pitches lit from elsewhere, such as a MIDI input or a sequencer, on top of the keys pressed here. |
| `disabled` | `boolean` |  |  |
| `layout` | `Layout` | `"keyboard"` | How the keys share the length: like a piano, or one equal share each. |
| `maxPitch` | `number` | `72` | Highest MIDI pitch, inclusive. |
| `minPitch` | `number` | `48` | Lowest MIDI pitch, inclusive: 60 is middle C. |
| `onNoteOff` | `(pitch: number, eventDetails: NoteOffEventDetails) => void` |  | Fires when a key is released, the pointer glides off it, or it loses focus while held. |
| `onNoteOn` | `(pitch: number, eventDetails: NoteOnEventDetails) => void` |  | Fires when a key is pressed or glided onto. `eventDetails.cancel()` keeps it silent. |
| `orientation` | `"horizontal" \| "vertical"` | `"horizontal"` |  |
| `pitches` | `readonly number[]` |  | Only these pitches get keys, instead of `minPitch`…`maxPitch`. |

**Data attributes**

| Attribute | Description |
| --- | --- |
| `data-disabled` | Present when disabled. |

### Keys

Renders `children` once per key, lowest first: the place to put a `Piano.Key` for each pitch. Renders no element of its own.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children` | `(pitch: number) => React.ReactNode` |  |  |

### Key

One key, placed along the keyboard for its pitch. It sounds while pressed: by pointer through the keyboard, or with Enter or Space while focused. Its accessible name defaults to the pitch, such as "C4". Renders a `<button>` element.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `pitch` | `number` |  | The MIDI pitch this key plays. |

**Data attributes**

| Attribute | Description |
| --- | --- |
| `data-active` | Whether the key is sounding: pressed here, or listed in `activeNotes`. |
| `data-disabled` | Present when disabled. |

### KeyLabel

The name of the key it sits in, such as "C4". Hidden from assistive technology, which reads the key's own name. Renders a `<span>` element.

**Data attributes**

| Attribute | Description |
| --- | --- |
| `data-disabled` | Present when disabled. |

### Registry component

`Piano` accepts style props (`size`, `className`) plus primitive props from `PianoPrimitive.Root.Props`.

#### Value and state

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `minPitch` | `number` | `48` | Lowest MIDI pitch, inclusive: 60 is middle C. |
| `maxPitch` | `number` | `72` | Highest MIDI pitch, inclusive. |
| `pitches` | `number[]` |  | Only these pitches get keys, instead of `minPitch`…`maxPitch`. |
| `activeNotes` | `number[]` |  | Pitches lit from elsewhere, such as a MIDI input or a sequencer, on top of the keys pressed here. |
| `disabled` | `boolean` | `false` | Whether the keyboard is disabled. |
| `onNoteOn` | `(pitch: number, eventDetails) => void` |  | Fires when a key is pressed or glided onto. `eventDetails.cancel()` keeps it silent. |
| `onNoteOff` | `(pitch: number, eventDetails) => void` |  | Fires when a key is released, the pointer glides off it, or it loses focus while held. |
| `aria-label` | `string` |  | Accessible name for the keyboard. |
| `aria-labelledby` | `string` |  | ID of a labeling element. |

#### Layout and display

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `size` | `"sm" \| "default" \| "lg"` | `"default"` | Size variant (sets the keyboard height). |
| `className` | `string` |  | Additional CSS classes. |

Additional valid HTML attributes passed to `Piano.Root` are forwarded.

## Examples

### External notes via activeNotes

```tsx
"use client";

import { useEffect, useState } from "react";
import { Piano } from "@/components/audio-ui/piano";

const CHORD_NOTES = [48, 52, 55];
const TOGGLE_MS = 900;

/** Simulated external source (a MIDI input, a sequencer) driving activeNotes independently of pointer presses. */
export function PianoActiveNotesDemo() {
  const [chordOn, setChordOn] = useState(true);

  useEffect(() => {
    const intervalId = window.setInterval(() => {
      setChordOn((current) => !current);
    }, TOGGLE_MS);
    return () => window.clearInterval(intervalId);
  }, []);

  return (
    <Piano
      activeNotes={chordOn ? CHORD_NOTES : []}
      aria-label="Piano"
      maxPitch={72}
      minPitch={48}
    />
  );
}
```
