Files
skills/docs/ui/react/react-control-surface/src/controls/piano-key/piano-key.md
2026-05-29 08:20:56 +01:00

4.7 KiB
Raw Blame History

<PianoKey>

(no submodules)

Purpose

A single piano key. Two visual kinds (white / black) and three visual states (idle / pressed / highlighted). Consumers compose many keys to build a full keyboard component — <PianoKey> itself deliberately renders a single rect so it stays compositional, theming flexible, and reusable in non-piano contexts (e.g. step sequencer cells themed as keys). The component implements BoundControlProps and emits onChange(max) on press, onChange(min) on release.

Features

  • Pointer and keyboard activation (Space, Enter).
  • Pointer-leave release (no stuck keys when you glide off the key).
  • Key-auto-repeat suppression via active-ref tracking.
  • Explicit state prop overrides derived state so callers can paint a key as highlighted (e.g. for chord-shape display) without changing value.
  • Default dimensions tuned per kind (white: 32×140, black: 20×90).
  • role="button" with aria-pressed reflecting the current state.
  • CSS-variable theming: --cs-piano-white, --cs-piano-white-pressed, --cs-piano-white-highlight, --cs-piano-white-stroke, --cs-piano-black, --cs-piano-black-pressed, --cs-piano-black-highlight, --cs-piano-black-stroke.

Usage: Basic

import { useState } from "react";
import { PianoKey } from "@suaveplan/react-control-surface";

export function MiddleC() {
    const [v, setV] = useState(0);
    return (
        <PianoKey
            value={v}
            onChange={setV}
            min={0}
            max={1}
            note={60}
        />
    );
}

Usage: Advanced

Compose an octave with black keys overlaid on whites:

import { useState } from "react";
import { PianoKey } from "@suaveplan/react-control-surface";

export function Octave() {
    const [pressed, setPressed] = useState<Set<number>>(new Set());
    const flip = (n: number, on: boolean) =>
        setPressed((p) => {
            const next = new Set(p);
            if (on) next.add(n);
            else next.delete(n);
            return next;
        });
    const whites = [0, 2, 4, 5, 7, 9, 11];
    const blacks = [{ off: 1, x: 22 }, { off: 3, x: 54 }, { off: 6, x: 118 }];
    return (
        <div style={{ position: "relative", height: 140, width: 224 }}>
            {whites.map((o, i) => (
                <div key={o} style={{ position: "absolute", left: i * 32 }}>
                    <PianoKey
                        value={pressed.has(60 + o) ? 1 : 0}
                        onChange={(v) => flip(60 + o, v > 0)}
                        min={0}
                        max={1}
                        note={60 + o}
                    />
                </div>
            ))}
            {blacks.map(({ off, x }) => (
                <div key={off} style={{ position: "absolute", left: x }}>
                    <PianoKey
                        kind="black"
                        value={pressed.has(60 + off) ? 1 : 0}
                        onChange={(v) => flip(60 + off, v > 0)}
                        min={0}
                        max={1}
                        note={60 + off}
                    />
                </div>
            ))}
        </div>
    );
}

API Reference

Prop Type Default
value number required
onChange (v: number) => void required
min/max number required
disabled boolean false
kind "white" | "black" "white"
state "idle" | "pressed" | "highlighted" derived
note number —
label string —
ariaLabel string —
widthPx number per-kind
heightPx number per-kind
className string —

Implementation Notes

The active state tracks an internal ref rather than React state so the press / release pair is event-loop-stable and immune to auto-repeat storms from held keys. Pointer capture is set on pointerdown and released on the matching pointerup / pointercancel / pointerleave, which gives correct release semantics even when the pointer drifts off the SVG (touch glissando). The explicit state prop fully overrides the derived state so the component can be driven from an external signal (incoming MIDI from another player, computer-aided visualisation) without coupling value to the visual state.