# XY pad

> The <audio-ui-xypad> custom element of @audio-ui/elements: its attributes, its parts and a live preview.

`<audio-ui-xypad>` controls a two-dimensional value `{ x, y }`. A press moves the point to the pointer and a drag follows it; the wheel, the trackpad and the arrow keys move it; a double click or Delete resets it. The position is exposed as `--xy-pad-x` (from the left) and `--xy-pad-y` (from the top), as percentages.

## Usage

{/* usage:start: written by `bun scripts/element-usage.ts --write` */}

The styles read the theme's CSS variables, which [Styling](/docs/elements#styling) sets.

**JavaScript**

*With CSS*

```html
<style>
  audio-ui-xypad {
    display: block;
    width: calc(var(--spacing, 0.25rem) * 48);
  }

  audio-ui-xypad-control {
    position: relative;
    display: block;
    aspect-ratio: 1;
    overflow: hidden;
    border-radius: calc(var(--radius, 0.625rem) - 2px);
    background: var(--muted, oklch(0.97 0 0));
    cursor: crosshair;
  }

  /* The crosshair follows the point. */
  audio-ui-xypad-control::before,
  audio-ui-xypad-control::after {
    content: "";
    position: absolute;
    background: var(--border, oklch(0.922 0 0));
    pointer-events: none;
  }

  audio-ui-xypad-control::before {
    top: 0;
    bottom: 0;
    left: var(--xy-pad-x);
    width: 1px;
  }

  audio-ui-xypad-control::after {
    top: var(--xy-pad-y);
    right: 0;
    left: 0;
    height: 1px;
  }

  audio-ui-xypad-thumb {
    position: absolute;
    z-index: 1;
    width: calc(var(--spacing, 0.25rem) * 4);
    height: calc(var(--spacing, 0.25rem) * 4);
    border-radius: var(--radius, 0.625rem);
    background: var(--primary, oklch(0.205 0 0));
    box-shadow: 0 0 0 4px color-mix(in oklab, var(--primary, oklch(0.205 0 0)) 20%, transparent);
  }

  audio-ui-xypad-value {
    display: block;
    margin-top: calc(var(--spacing, 0.25rem) * 2);
    color: var(--muted-foreground, oklch(0.556 0 0));
    font-family: var(--font-mono, ui-monospace, monospace);
    font-size: 0.75rem;
    line-height: 1rem;
    font-variant-numeric: tabular-nums;
    text-align: center;
  }
</style>

<audio-ui-xypad value="30 70">
  <audio-ui-xypad-control>
    <audio-ui-xypad-thumb aria-label="Filter"></audio-ui-xypad-thumb>
  </audio-ui-xypad-control>
  <audio-ui-xypad-value></audio-ui-xypad-value>
</audio-ui-xypad>

<script type="module">
  import "@audio-ui/elements/xypad";

  const xypad = document.querySelector("audio-ui-xypad");
  xypad.addEventListener("input", (event) =>
    console.log(event.detail.value)
  );
</script>
```

*With Tailwind*

```html
<audio-ui-xypad class="block w-48" value="30 70">
  <audio-ui-xypad-control class="relative block aspect-square cursor-crosshair overflow-hidden rounded-md bg-muted before:pointer-events-none before:absolute before:inset-y-0 before:left-(--xy-pad-x) before:w-px before:bg-border before:content-[''] after:pointer-events-none after:absolute after:inset-x-0 after:top-(--xy-pad-y) after:h-px after:bg-border after:content-['']">
    <audio-ui-xypad-thumb class="absolute z-1 size-4 rounded-lg bg-primary ring-4 ring-primary/20" aria-label="Filter"></audio-ui-xypad-thumb>
  </audio-ui-xypad-control>
  <audio-ui-xypad-value class="mt-2 block text-center font-mono text-muted-foreground text-xs tabular-nums"></audio-ui-xypad-value>
</audio-ui-xypad>

<script type="module">
  import "@audio-ui/elements/xypad";

  const xypad = document.querySelector("audio-ui-xypad");
  xypad.addEventListener("input", (event) =>
    console.log(event.detail.value)
  );
</script>
```

**Vue**

*With CSS*

```vue
<script setup lang="ts">
import "@audio-ui/elements/xypad";
import { ref } from "vue";

const point = ref({ x: 30, y: 70 });
</script>

<template>
  <audio-ui-xypad v-model="point">
    <audio-ui-xypad-control>
      <audio-ui-xypad-thumb aria-label="Filter" />
    </audio-ui-xypad-control>
    <audio-ui-xypad-value />
  </audio-ui-xypad>
</template>

<style>
audio-ui-xypad {
  display: block;
  width: calc(var(--spacing, 0.25rem) * 48);
}

audio-ui-xypad-control {
  position: relative;
  display: block;
  aspect-ratio: 1;
  overflow: hidden;
  border-radius: calc(var(--radius, 0.625rem) - 2px);
  background: var(--muted, oklch(0.97 0 0));
  cursor: crosshair;
}

/* The crosshair follows the point. */
audio-ui-xypad-control::before,
audio-ui-xypad-control::after {
  content: "";
  position: absolute;
  background: var(--border, oklch(0.922 0 0));
  pointer-events: none;
}

audio-ui-xypad-control::before {
  top: 0;
  bottom: 0;
  left: var(--xy-pad-x);
  width: 1px;
}

audio-ui-xypad-control::after {
  top: var(--xy-pad-y);
  right: 0;
  left: 0;
  height: 1px;
}

audio-ui-xypad-thumb {
  position: absolute;
  z-index: 1;
  width: calc(var(--spacing, 0.25rem) * 4);
  height: calc(var(--spacing, 0.25rem) * 4);
  border-radius: var(--radius, 0.625rem);
  background: var(--primary, oklch(0.205 0 0));
  box-shadow: 0 0 0 4px color-mix(in oklab, var(--primary, oklch(0.205 0 0)) 20%, transparent);
}

audio-ui-xypad-value {
  display: block;
  margin-top: calc(var(--spacing, 0.25rem) * 2);
  color: var(--muted-foreground, oklch(0.556 0 0));
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: 0.75rem;
  line-height: 1rem;
  font-variant-numeric: tabular-nums;
  text-align: center;
}
</style>
```

*With Tailwind*

```vue
<script setup lang="ts">
import "@audio-ui/elements/xypad";
import { ref } from "vue";

const point = ref({ x: 30, y: 70 });
</script>

<template>
  <audio-ui-xypad class="block w-48" v-model="point">
    <audio-ui-xypad-control class="relative block aspect-square cursor-crosshair overflow-hidden rounded-md bg-muted before:pointer-events-none before:absolute before:inset-y-0 before:left-(--xy-pad-x) before:w-px before:bg-border before:content-[''] after:pointer-events-none after:absolute after:inset-x-0 after:top-(--xy-pad-y) after:h-px after:bg-border after:content-['']">
      <audio-ui-xypad-thumb class="absolute z-1 size-4 rounded-lg bg-primary ring-4 ring-primary/20" aria-label="Filter" />
    </audio-ui-xypad-control>
    <audio-ui-xypad-value class="mt-2 block text-center font-mono text-muted-foreground text-xs tabular-nums" />
  </audio-ui-xypad>
</template>
```

**Svelte**

*With CSS*

```svelte
<script lang="ts">
  import "@audio-ui/elements/xypad";

  let point = $state({ x: 30, y: 70 });
</script>

<audio-ui-xypad value={point} oninput={(event) => (point = event.detail.value)}>
  <audio-ui-xypad-control>
    <audio-ui-xypad-thumb aria-label="Filter"></audio-ui-xypad-thumb>
  </audio-ui-xypad-control>
  <audio-ui-xypad-value></audio-ui-xypad-value>
</audio-ui-xypad>

<style>
:global {
  audio-ui-xypad {
    display: block;
    width: calc(var(--spacing, 0.25rem) * 48);
  }

  audio-ui-xypad-control {
    position: relative;
    display: block;
    aspect-ratio: 1;
    overflow: hidden;
    border-radius: calc(var(--radius, 0.625rem) - 2px);
    background: var(--muted, oklch(0.97 0 0));
    cursor: crosshair;
  }

  /* The crosshair follows the point. */
  audio-ui-xypad-control::before,
  audio-ui-xypad-control::after {
    content: "";
    position: absolute;
    background: var(--border, oklch(0.922 0 0));
    pointer-events: none;
  }

  audio-ui-xypad-control::before {
    top: 0;
    bottom: 0;
    left: var(--xy-pad-x);
    width: 1px;
  }

  audio-ui-xypad-control::after {
    top: var(--xy-pad-y);
    right: 0;
    left: 0;
    height: 1px;
  }

  audio-ui-xypad-thumb {
    position: absolute;
    z-index: 1;
    width: calc(var(--spacing, 0.25rem) * 4);
    height: calc(var(--spacing, 0.25rem) * 4);
    border-radius: var(--radius, 0.625rem);
    background: var(--primary, oklch(0.205 0 0));
    box-shadow: 0 0 0 4px color-mix(in oklab, var(--primary, oklch(0.205 0 0)) 20%, transparent);
  }

  audio-ui-xypad-value {
    display: block;
    margin-top: calc(var(--spacing, 0.25rem) * 2);
    color: var(--muted-foreground, oklch(0.556 0 0));
    font-family: var(--font-mono, ui-monospace, monospace);
    font-size: 0.75rem;
    line-height: 1rem;
    font-variant-numeric: tabular-nums;
    text-align: center;
  }
}
</style>
```

*With Tailwind*

```svelte
<script lang="ts">
  import "@audio-ui/elements/xypad";

  let point = $state({ x: 30, y: 70 });
</script>

<audio-ui-xypad class="block w-48" value={point} oninput={(event) => (point = event.detail.value)}>
  <audio-ui-xypad-control class="relative block aspect-square cursor-crosshair overflow-hidden rounded-md bg-muted before:pointer-events-none before:absolute before:inset-y-0 before:left-(--xy-pad-x) before:w-px before:bg-border before:content-[''] after:pointer-events-none after:absolute after:inset-x-0 after:top-(--xy-pad-y) after:h-px after:bg-border after:content-['']">
    <audio-ui-xypad-thumb class="absolute z-1 size-4 rounded-lg bg-primary ring-4 ring-primary/20" aria-label="Filter"></audio-ui-xypad-thumb>
  </audio-ui-xypad-control>
  <audio-ui-xypad-value class="mt-2 block text-center font-mono text-muted-foreground text-xs tabular-nums"></audio-ui-xypad-value>
</audio-ui-xypad>
```

**React**

In React, the same control is the [XYPad](/docs/react/xypad) primitive of `@audio-ui/react`, with the same parts as React components and its value as props.

{/* usage:end */}

## API reference

```bash
npm install @audio-ui/elements
```

```ts
import "@audio-ui/elements/xypad";
```

| Export | Kind | What it is |
| --- | --- | --- |
| `XYPAD_TAGS` | const | The names the XY pad's elements are registered under, frozen: a page cannot rename them by accident. |
| `XYPadChangeEvent` | type | Fired once a gesture ends with a new point. |
| `XYPadControlElement` | class | The interactive surface. |
| `XYPadElement` | class | The root of a two-dimensional control. |
| `XYPadElementEventMap` | interface | The events an XY pad fires, as `addEventListener` types them. |
| `XYPadEventDetail` | interface | What an XY pad's `input` and `change` events carry, besides the pad itself as their target. |
| `XYPadInputEvent` | type | Fired on every change. |
| `XYPadLabelElement` | class | The accessible label. |
| `XYPadThumbElement` | class | Marks the point, positioned with `left` and `top` and centered with `translate`. |
| `XYPadValueElement` | class | Renders the point as `"x, y"`, formatted with the pad's `format` and `locale`. |

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `min-x` | `number` | `0` |  |
| `max-x` | `number` | `100` |  |
| `min-y` | `number` | `0` |  |
| `max-y` | `number` | `100` |  |
| `step-x` | `number` | `1` |  |
| `step-y` | `number` | `1` |  |
| `value` | `string` |  | The point, as `"x y"`. The `value` property reads and sets it as `{ x, y }`. |
| `default-value` | `string` |  | The initial point, restored on reset. Defaults to the center. |
| `format` | `string` |  | `Intl.NumberFormat` options as JSON, applied to each axis. |
| `locale` | `string` |  | Defaults to the browser locale. |
| `name` | `string` |  | The inputs submit as `name.x` and `name.y`. |
| `disabled` | `boolean` |  |  |

`input` and `change` carry `{ value: { x, y }, reason }`.

| Part | Description |
| --- | --- |
| `<audio-ui-xypad-control>` | The interactive surface: press, drag, wheel, double click to reset |
| `<audio-ui-xypad-thumb>` | Marks the point and contains one native range input per axis. Its `aria-label` labels both axes ("Filter X", "Filter Y"); `label-x` and `label-y` label each one |
| `<audio-ui-xypad-value>` | Both axes, formatted as `"x, y"` |
| `<audio-ui-xypad-label>` | Labels both inputs when the thumb has no accessible name |

**In React**

The same control in React, and its parts' props: [XYPad](/docs/react/xypad).

