AngleDial

2 parts

An accessible circular angle / rotation picker. The root owns the angle value in DEGREES (controlled via v-model:value or uncontrolled via defaultValue), converts pointer presses anywhere on the dial into an angle, snaps to snap / step, and handles the 0 / 360 seam either by wrapping continuously (wrap) or bounding to an arc (clamp). It provides context to AngleDialThumb, which renders the role="slider" handle on the ring and owns keyboard interaction. The angle convention is fixed: points UP (12 o'clock) and increases CLOCKWISE (right = 90°, down = 180°, left = 270°). Reach for it for rotation, heading, or hue (a hue ring is min: 0, max: 360, wrap: 'wrap').

Demo

Loading demo…

Anatomy

Import the parts and compose them. Each part forwards attributes to its underlying element.

vue
import {
  AngleDialRoot,
  AngleDialThumb,
} from '@robonen/primitives/canvas/angle-dial';

<AngleDialRoot>
  <AngleDialThumb />
</AngleDialRoot>

API Reference

AngleDialRoot

Root

An accessible circular angle / rotation picker. The root owns the angle value in DEGREES (controlled via `v-model:value` or uncontrolled via `defaultValue`), converts pointer presses anywhere on the dial into an angle, snaps to `snap` / `step`, and handles the `0` / `360` seam either by wrapping continuously (`wrap`) or bounding to an arc (`clamp`). It provides context to `AngleDialThumb`, which renders the `role="slider"` handle on the ring and owns keyboard interaction. The angle convention is fixed: `0°` points UP (12 o'clock) and increases CLOCKWISE (right = 90°, down = 180°, left = 270°). Reach for it for rotation, heading, or hue (a hue ring is `min: 0, max: 360, wrap: 'wrap'`).

PropTypeDescription
min?numberMin angle in degrees.
max?numberMax angle in degrees.
step?numberStep granularity in degrees.
largeStep?numberIncrement for Page keys / Shift+Arrow, in degrees.
wrap?AngleDialWrapSeam behavior at the bounds. 'wrap' lets the value cross 0 / 360 continuously; 'clamp' bounds it to the arc [min, max].
snap?AngleDialSnapSnap increments in degrees — a scalar (e.g. 15) or an explicit list (e.g. [0, 45, 90, 135, 180, 225, 270, 315]). undefined disables snapping (only step rounding applies).
disabled?booleanDisable all interaction.
dir?AngleDialDirectionWriting direction. When omitted it is inherited from the nearest ConfigProvider (falling back to 'ltr'); an explicit value wins.
defaultValue?numberUncontrolled initial angle in degrees.
value?number | nullTwo-way bound value (v-model:value).
Emits
EventPayload
update:value[value: number | null]

AngleDialThumb

Thumb

The draggable handle of an `AngleDialRoot`, rendered as `role="slider"` and positioned on the ring by the current angle. It owns keyboard interaction (Arrow keys step by `step`, Shift+Arrow / Page keys by `largeStep`, Home / End jump to the bounds) and carries the ARIA value attributes. It INTENTIONALLY omits `aria-orientation`: a radial control has no single axis, so claiming `horizontal` / `vertical` would be misleading. Because a bare number is ambiguous on a circle, `aria-valuetext` strongly defaults to `` `${Math.round(deg)}°` `` (override via `valueText`). Give the thumb an `aria-label` (defaults to `'Angle'`). Exposes `value` and `point` (its fractional `{ x, y }` position on a unit circle, with `0.5,0.5` at center) as slot props for positioning.

PropTypeDescription
valueText?AngleDialValueTextFormatter producing a human-friendly aria-valuetext from the angle in degrees. Defaults to (deg) => `${Math.round(deg)}°` — a degree suffix disambiguates the bare number on a circular control. Return undefined to omit aria-valuetext.