HyAnglePicker
Usage examples on this page are written for Lit / plain HTML (
<hy-angle-picker>). The same component ships asHyAnglePickerin@whitespaceux/harmony-react(native React 19), with typed wrappers for Angular, Solid, Svelte, and Vue. The API reference below applies to all frameworks.
A circular angle picker for inputs that represent a direction — shadow light angle, gradient direction, hue rotation, anything naturally circular. The control reads as a clock face: cardinal labels around the rim, an indicator line pointing from centre to the current angle. Drag the rim, click anywhere inside to snap to that direction, or use arrow keys for fine adjustment.
Form Integration
Implements the Form-Associated Custom Elements API — the current angle (in degrees) is included in FormData under name when the containing form submits. A hidden native <input type="range"> inside the shadow root carries the focus, fires change on commit, and surfaces the value to assistive tech via aria-valuetext.
Why a circle, not a slider
Angles are circular: 0° wraps back to itself; there's no real start or end. A linear slider with 0 on the left and 360 on the right fights that mental model — moving from "Left" forward should land back at "Top", not run off the right edge. Every shipping design tool that takes an angle (Figma, Photoshop, Sketch, After Effects, every CSS colour wheel for hue) uses a circular control for this reason.
Keyboard
The hidden native range input handles keyboard:
- Arrow keys: ±
step(1° default) - Shift + arrows: ±45° — snap toward the next cardinal
- Home / End: 0° / 180°
- PageUp / PageDown: ±15° (mid-coarse for finer dragging)
Sparse tick marks
Pass tickMarks to label specific compass positions. The picker reuses the slider's TickMark shape so designers learn one vocabulary for "named stops on a numeric range" regardless of the control's geometry. Labels at each angle are auto-positioned on a ring just outside the rim.
Examples
Basic usage
<hy-angle-picker label="Light angle" value="45"></hy-angle-picker>Compass-labelled dial
const picker = document.querySelector('hy-angle-picker'); picker.tickMarks = [ { value: 0, label:
'T', ariaLabel: 'Top' }, { value: 90, label: 'R', ariaLabel: 'Right' }, { value: 180, label: 'B',
ariaLabel: 'Bottom' }, { value: 270, label: 'L', ariaLabel: 'Left' }, ];<hy-angle-picker label="Light angle"></hy-angle-picker>Snap-to-cardinals (no diagonals)
picker.tickMarks = [ { value: 0, label: 'T' }, { value: 90, label: 'R' }, { value: 180, label: 'B'
}, { value: 270, label: 'L' }, ];<hy-angle-picker label="Direction" tick-snap></hy-angle-picker>Form submission
<form>
<hy-angle-picker name="light-angle" value="135"></hy-angle-picker>
<button type="submit">Save</button>
</form>API
Properties
| Property | Attribute | Type | Default | Description |
|---|---|---|---|---|
value | value | number | 0 | Current angle in compass degrees [0, 360). Values outside this range are wrapped on write. |
step | step | number | 1 | Granularity for keyboard arrow-key adjustments. Default 1° — pointer-driven changes ignore step and operate at pixel precision. |
disabled | disabled | boolean | false | Disables the picker. |
error | error | boolean | false | Visual error state. |
name | name | string | '' | The name attribute for form submission. |
label | label | string | '' | Primary label displayed above the dial. |
hint | hint | string | '' | Secondary hint text displayed under the label. |
helperText | helper-text | string | '' | Helper text displayed below the dial. |
errorMessage | error-message | string | '' | Error message shown when error is true (auto-sets error). |
size | size | 'small' | 'medium' | 'large' | 'medium' | Size variant affecting dial diameter. |
variant | variant | 'default' | 'success' | 'warning' | 'danger' | 'default' | Visual variant affecting indicator + rim colour. |
tickMarks | — | TickMark[] | [] | Sparse named tick marks positioned around the rim. Same shape as hy-slider's tickMarks — { value, label?, ariaLabel? }. Property-only (arrays can't round-trip via HTML attribute); set via Lit .tickMarks=${[...]} or framework property binding. See TickMark (re-exported from this module). |
tickSnap | tick-snap | boolean | false | When true, the picker snaps to the values declared in tickMarks during pointer drag — step still governs keyboard granularity. Defaults to false (tick marks are visual references only). No-op when tickMarks is empty. |
inputId | input-id | string | '' | Explicit ID for the underlying input — auto-generated if empty. |
autofocus | autofocus | boolean | false | Marks this picker as the auto-focus target. Read by hy-dialog (and other overlay containers) on show to choose initial focus, and applied to the inner native input on first connect. |
form | — | HTMLFormElement | null | — | — |
validity | — | ValidityState | — | — |
validationMessage | — | string | — | — |
Events
| Event | Detail | Description |
|---|---|---|
input | — | Fired during drag / keyboard adjustment. Detail: { value: number } |
change | — | Fired when the value is committed (pointer release / blur). Detail: { value: number } |
CSS Parts
| Part | Description |
|---|---|
base | The outer wrapper (label + dial + helper text column) |
label | The label container (label + hint) |
dial | The clickable circle that owns the rim + indicator + labels |
input | The hidden native range input |
rim | The visible circle border |
indicator | The line from centre to the current angle |
thumb | The thumb dot at the tip of the indicator |
hub | The small disc at the centre of the dial |
tick-marks | Container for the rim tick lines (only present when tickMarks has entries) |
tick-mark | Individual tick line on the rim |
tick-mark-labels | Container for tick labels positioned around the rim |
tick-mark-label | Individual label outside the rim |
helper-text | The helper text element |
error-message | The error message element |
CSS Custom Properties
| Property | Description |
|---|---|
--hy-angle-picker-size-sm | Dial diameter for size=small. Default: 4.5rem |
--hy-angle-picker-size-md | Dial diameter for size=medium. Default: 6.5rem |
--hy-angle-picker-size-lg | Dial diameter for size=large. Default: 9rem |
--hy-angle-picker-rim-color | Rim stroke colour |
--hy-angle-picker-rim-width | Rim stroke width |
--hy-angle-picker-fill-color | Optional fill behind the indicator (defaults to transparent) |
--hy-angle-picker-indicator-color | Colour of the indicator line + hub |
--hy-angle-picker-indicator-width | Indicator stroke width |
--hy-angle-picker-hub-ornament | Diameter of the centre disc |
--hy-angle-picker-thumb-ornament | Diameter of the draggable disc at the indicator tip |
--hy-angle-picker-tick-color | Colour of rim tick lines (only when tickMarks is set) |
--hy-angle-picker-tick-label-color | Colour of rim tick labels |
--hy-angle-picker-label-radius | Radial distance from centre at which tick labels render. Default: dial radius + 0.75em |
--hy-angle-picker-hover-duration | Transition duration for hover and focus state feedback |
--hy-angle-picker-hover-easing | Transition easing for hover and focus state feedback |