addstack

Keys

Component 5

About

A headless piano keyboard that plays with pointer glissando, touch and the keyboard, reports presses with velocity, and can show notes held elsewhere (MIDI input or playback) without re-rendering.

Built with React and Tailwind. Copy the prompt and paste it into Claude Code, Cursor or Lovable: your agent adds Keys to your project and fits it to your theme.

Code

How Keys is used: the demo's code. The component's own source comes with a free account.

Usage.tsx

"use client";

import Keys from "@/components/ui/keys";
import { formats } from "@/components/ui/keys-utils/format";
import { useRef } from "react";

const pitch = formats.pitch({
  names: ["C", "C♯", "D", "D♯", "E", "F", "F♯", "G", "G♯", "A", "A♯", "B"],
});
// C3 to C5.
const range = [48, 72] as const;
const notes = Array.from(
  { length: range[1] - range[0] + 1 },
  (_, index) => range[0] + index,
);

/** A small synth for the demo: a triangle wave per key, as loud as the key was pressed. In an app, your audio engine. */
function useSynth() {
  const audio = useRef<AudioContext | null>(null);
  const voices = useRef(
    new Map<number, { oscillator: OscillatorNode; gain: GainNode }>(),
  );
  return {
    play(note: number, velocity: number) {
      const context = (audio.current ??= new AudioContext());
      const oscillator = new OscillatorNode(context, {
        type: "triangle",
        frequency: 440 * 2 ** ((note - 69) / 12),
      });
      const gain = new GainNode(context, { gain: 0 });
      gain.gain.setTargetAtTime(0.25 * velocity, context.currentTime, 0.005);
      oscillator.connect(gain).connect(context.destination);
      oscillator.start();
      voices.current.set(note, { oscillator, gain });
    },
    stop(note: number) {
      const voice = voices.current.get(note);
      if (!voice || !audio.current) return;
      voices.current.delete(note);
      voice.gain.gain.setTargetAtTime(0, audio.current.currentTime, 0.1);
      voice.oscillator.stop(audio.current.currentTime + 1);
    },
  };
}

export default function KeysDemo() {
  const synth = useSynth();
  return (
    <Keys.Root
      range={range}
      format={pitch}
      onPress={(note, { velocity }) => synth.play(note, velocity)}
      onRelease={(note) => synth.stop(note)}
      aria-label="Keyboard"
      className="h-36 w-full max-w-xl"
    >
      {notes.map((note) => (
        <Keys.Key
          key={note}
          note={note}
          className="flex items-end justify-center rounded-b-md border border-neutral-300 bg-white pb-1.5 text-[10px] text-neutral-400 outline-offset-[-3px] focus-visible:outline-2 focus-visible:outline-orange-500 data-black:h-3/5 data-black:border-neutral-950 data-black:bg-neutral-900 data-pressed:bg-orange-200 data-black:data-pressed:bg-orange-500 dark:border-neutral-700 dark:bg-neutral-200 dark:data-pressed:bg-orange-300"
        >
          {/* A label on each C, from the same format that names the keys. */}
          {note % 12 === 0 && pitch.format(note)}
        </Keys.Key>
      ))}
    </Keys.Root>
  );
}