XYPad
A two-dimensional slider for setting a pair of values with one gesture — a focal point, a shadow offset, a filter pad.
import { XYPad } from '@ontoui/react';
<XYPad.Root defaultValue={{ x: 50, y: 50 }}> <XYPad.Label>Focal point</XYPad.Label> <XYPad.Value /> <XYPad.Control> <XYPad.Grid /> <XYPad.Thumb /> </XYPad.Control></XYPad.Root>;Anatomy
| Part | Element | Description |
|---|---|---|
XYPad.Root | <div> | Groups all parts and holds the pad state. |
XYPad.Label | <div> | An accessible label associated with both axes. |
XYPad.Value | <output> | Displays the current point as text. |
XYPad.Control | <div> | The square, interactive surface. Sizes itself from its width. |
XYPad.Grid | <div> | Optional decorative grid lines drawn across the surface. |
XYPad.Thumb | <div> | The draggable point, and the pad’s two keyboard and form controls. |
The value is a point — { x, y } — and each axis has its own range: minX, maxX, stepX
and largeStepX, with minY, maxY, stepY and largeStepY alongside them. Both axes
default to 0–100 with a step of 1.
XYPad.Control is square (aspect-ratio: 1) and fills the width it is given, so size the pad
by setting a width on XYPad.Root.
Examples
Vertical direction
y grows upwards by default, putting maxY at the top edge — the convention for a filter or
EQ pad. Set yDirection="down" for values that map straight to a screen offset, such as a
shadow, where maxY belongs at the bottom.
XYPad.Value takes a render prop to format the point, and XYPad.Thumb takes xLabel and
yLabel to name the axes for screen readers.
const [offset, setOffset] = useState({ x: 8, y: 12 });
<XYPad.Root value={offset} onValueChange={setOffset} minX={-24} maxX={24} minY={-24} maxY={24} yDirection="down"> <XYPad.Label>Shadow offset</XYPad.Label> <XYPad.Value>{(value) => `${value.x}px, ${value.y}px`}</XYPad.Value> <XYPad.Control> <XYPad.Grid columns={6} rows={6} /> <XYPad.Thumb xLabel="Horizontal offset" yLabel="Vertical offset" /> </XYPad.Control></XYPad.Root>;Snapping to a grid
stepX and stepY snap each axis as the pointer moves. Give XYPad.Grid a matching number of
columns and rows to draw the positions the thumb will land on. Pass 0 as a step to leave
an axis continuous.
<XYPad.Root defaultValue={{ x: 30, y: 70 }} stepX={10} stepY={10}> <XYPad.Label>Position</XYPad.Label> <XYPad.Value /> <XYPad.Control> <XYPad.Grid columns={10} rows={10} /> <XYPad.Thumb /> </XYPad.Control></XYPad.Root>Disabled
Set disabled on XYPad.Root to prevent interaction.
<XYPad.Root defaultValue={{ x: 40, y: 60 }} disabled> <XYPad.Label>Focal point</XYPad.Label> <XYPad.Value /> <XYPad.Control> <XYPad.Grid /> <XYPad.Thumb /> </XYPad.Control></XYPad.Root>In a form
nameX and nameY name the two values in the submitted form data. Without them the pad holds
no form value.
<XYPad.Root nameX="focalX" nameY="focalY" defaultValue={{ x: 50, y: 50 }}> <XYPad.Label>Focal point</XYPad.Label> <XYPad.Control> <XYPad.Grid /> <XYPad.Thumb /> </XYPad.Control></XYPad.Root>Keyboard
XYPad.Thumb renders one range input per axis, so the pad has two tab stops and each value is
announced and adjustable on its own. Both inputs answer to all four arrow keys — otherwise half
the plane would be out of reach from whichever axis held focus.
| Key | Action |
|---|---|
| ← → | Move by stepX. |
| ↑ ↓ | Move the thumb up or down the pad by stepY. |
| Shift + arrow | Move by the large step, ten steps by default. |
| Page Up / Page Down | Move the focused axis by its large step. |
| Home / End | Jump the focused axis to its minimum or maximum. |
Arrow keys move the thumb in the direction pressed, so ↑ decreases y when
yDirection is down. Page Up and Page Down step the value itself and are unaffected by the
direction.
Accessibility
- Always render
XYPad.Label— it names both axes. Each input’s accessible name is the pad label followed by its axis name, so a labelled pad announces “Shadow offset, Horizontal”. - Rename the axes with
xLabelandyLabelonXYPad.Thumbwhen “Horizontal” and “Vertical” don’t describe what the pad controls, or to translate them. XYPad.Gridis decorative and hidden from assistive tech. Nothing about the value depends on it, so a pad without one behaves identically.
API Reference
XYPad.Root
| Prop | Type | Default |
|---|---|---|
children? | ReactNode | — |
className? | string | — |
style? | CSSProperties | — |
value? | XYPadValue | — |
defaultValue? | XYPadValue | { x: minX, y: minY } |
onValueChange? | (value: XYPadValue) => void | — |
onValueCommitted? | (value: XYPadValue) => void | — |
minX? | number | 0 |
maxX? | number | 100 |
stepX? | number | 1 |
largeStepX? | number | stepX * 10 |
minY? | number | 0 |
maxY? | number | 100 |
stepY? | number | 1 |
largeStepY? | number | stepY * 10 |
yDirection? | "up" | "down" | 'up' |
disabled? | boolean | false |
nameX? | string | — |
nameY? | string | — |
ref? | Ref<HTMLDivElement> | — |
XYPad.Label
| Prop | Type | Default |
|---|---|---|
children? | ReactNode | — |
className? | string | — |
style? | CSSProperties | — |
ref? | Ref<HTMLDivElement> | — |
XYPad.Value
| Prop | Type | Default |
|---|---|---|
className? | string | — |
style? | CSSProperties | — |
children? | (value: XYPadValue) => ReactNode | (value) => `${value.x}, ${value.y}` |
ref? | Ref<HTMLDivElement> | — |
XYPad.Control
| Prop | Type | Default |
|---|---|---|
children? | ReactNode | — |
className? | string | — |
style? | CSSProperties | — |
ref? | Ref<HTMLDivElement> | — |
XYPad.Grid
| Prop | Type | Default |
|---|---|---|
className? | string | — |
style? | CSSProperties | — |
columns? | number | 4 |
rows? | number | 4 |
ref? | Ref<HTMLDivElement> | — |
XYPad.Thumb
| Prop | Type | Default |
|---|---|---|
className? | string | — |
style? | CSSProperties | — |
xLabel? | string | 'Horizontal' |
yLabel? | string | 'Vertical' |
ref? | Ref<HTMLDivElement> | — |