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

4.2 KiB

<XYPad> — 2-axis touch surface

(no submodules)

Purpose

<XYPad> is an accessible 2-axis touch surface that emits normalised { x, y } coordinates as the user drags a pointer or steps with the arrow keys. The Y axis follows musical-software convention — the top of the pad is y = 1 and the bottom is y = 0. Both axes are normalised to [0, 1] regardless of the pad's pixel size, so the caller can map them onto any parameter range with a simple scale.

Common applications include morphing two modulation sources, mapping filter cutoff against resonance, controlling pan + send, or any pair of correlated parameters that benefit from a single gestural surface.

Features

  • Pointer-drag emission via Pointer Events with capture for off-pad drags
  • Keyboard navigation — Arrows step by step, PageUp/PageDown by step * 4, Home/End jump the X axis to the extremes
  • Full Tab focusability with role="application"
  • Clamping to [0, 1] on every emission, including out-of-bounds drags
  • disabled removes the element from the Tab order and silences events
  • CSS-variable themed background and cursor
  • data-x / data-y deterministic attributes for E2E inspection

Usage: Basic

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

function Pad() {
    const [v, setV] = useState({ x: 0.5, y: 0.5 });
    return <XYPad value={v} onChange={setV} label="Morph" />;
}

Usage: Advanced

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

function FilterPad({
    onCutoff,
    onResonance,
}: {
    onCutoff: (hz: number) => void;
    onResonance: (q: number) => void;
}) {
    const [v, setV] = useState({ x: 0.5, y: 0.5 });
    return (
        <XYPad
            value={v}
            onChange={(next) => {
                setV(next);
                onCutoff(40 + next.x * 19960);
                onResonance(0.5 + next.y * 9.5);
            }}
            step={0.01}
            width={320}
            height={240}
            label="Filter morph"
        />
    );
}

API Reference

Prop Type Default Description
value { x: number; y: number } req Current normalised value, both in [0, 1].
onChange (next: XYValue) => void req Called for every drag and keyboard step.
step number 0.05 Step size for arrow keys.
width number 200 Render width in CSS pixels.
height number 200 Render height in CSS pixels.
disabled boolean false When true, ignores all input.
label string — Visible + accessible label.
ariaLabel string derived Override the accessible label.
className string — Appended to rcs-xy-pad.

The exported pointerToValue(rect, clientX, clientY) helper performs the pure rect-to-[0, 1] conversion (including Y-inversion and clamping) and is exported for re-use in custom pads.

Implementation Notes

Pointer events are used uniformly across mouse + touch + pen. On pointerdown the pad calls setPointerCapture so subsequent drags outside the bounding rect continue to fire pointermove events. The draggingRef flag avoids spurious moves before the user has pressed.

Keyboard handling is exhaustive: every recognised key calls preventDefault and emits a clamped value; unrecognised keys do nothing (the event bubbles for the consumer to handle). The aria-valuetext attribute is updated on every render with the current x/y truncated to two decimals, providing screen-reader support without a polling cycle.