Skip to content

HyAnglePicker ​

Usage examples on this page are written for Lit / plain HTML (<hy-angle-picker>). The same component ships as HyAnglePicker in @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 center to the current angle. Drag the rim, click anywhere inside to snap to that direction, or use arrow keys for fine adjustment. The degrees read beside the dial, so a drag lands near the angle and the arrow keys finish it exactly.

The readout ​

The number beside the dial is the slider's readout: the value in the locale's digits with suffix after it (the degree sign by default), display-precision decimals (inferred from step when unset), or whatever formatValue returns; hide-value drops it. Its column is reserved for the widest angle so the dial never shifts while dragging. The same string is what a screen reader hears when the needle is off a named tick. A field to type an exact angle stays a hy-number-input beside the dial.

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 color 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.

When to use

  • A direction or a rotation: a gradient angle, a light direction, an image rotation.

Do

  • Use tick-snap when only some angles are useful.

When not to use

  • People who already know the exact degree: hy-number-input. Put one beside the dial when both help.
  • A linear amount such as opacity, size or duration: hy-slider. A dial says the two ends meet.

Accessibility: your part ​

  • Name the dial with label, or with aria-label on the host when the text around it already names it.
  • Give each entry in tickMarks a label or an ariaLabel, so keyboard and screen-reader users hear the named direction, not only degrees.

Examples ​

Basic usage ​

html
<hy-angle-picker label="Light angle" value="45"></hy-angle-picker>

The readout ​

The degrees read beside the dial by default. Change the unit, the precision or hide it.

html
<hy-angle-picker label="Light angle" value="45"></hy-angle-picker>
<hy-angle-picker label="Rotation" value="22.5" step="0.5"></hy-angle-picker>
<hy-angle-picker label="Heading" value="45" suffix=" deg"></hy-angle-picker>
<hy-angle-picker label="Direction" value="90" hide-value></hy-angle-picker>

Compass-labeled dial ​

javascript
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' },
];
html
<hy-angle-picker label="Light angle"></hy-angle-picker>

Snap-to-cardinals (no diagonals) ​

javascript
picker.tickMarks = [
  { value: 0, label: 'T' },
  { value: 90, label: 'R' },
  { value: 180, label: 'B' },
  { value: 270, label: 'L' },
];
html
<hy-angle-picker label="Direction" tick-snap></hy-angle-picker>

Error and warning ​

Setting error-message raises error: the indicator and the rim take the danger color and the input is marked invalid. warning-message raises warning: the same treatment in the warning color, announced politely, and the value stays valid. error wins when both are set.

html
<hy-angle-picker
  label="Light angle"
  value="200"
  error-message="Light from below is not supported"
></hy-angle-picker>
<hy-angle-picker
  label="Light angle"
  value="170"
  warning-message="Light from below reads as unnatural"
></hy-angle-picker>

Form submission ​

html
<form>
  <hy-angle-picker name="light-angle" value="135"></hy-angle-picker>
  <button type="submit">Save</button>
</form>

API ​

Properties ​

PropertyAttributeTypeDefaultDescription
valuevaluenumber0Current angle in compass degrees [0, 360). Values outside this range are wrapped on write.
stepstepnumber1Granularity for keyboard arrow-key adjustments. Default 1° — pointer-driven changes ignore step and operate at pixel precision.
disableddisabledbooleanfalseDisables the picker.
errorerrorbooleanfalseError state: the value is not accepted. Paints the indicator and rim in the danger color and sets aria-invalid.
warningwarningbooleanfalseWarning state: the value is accepted but worth a second look. Paints the indicator and rim in the warning color; never sets aria-invalid and never blocks submission. error wins when both are set.
namenamestring''The name attribute for form submission.
labellabelstring''Primary label displayed above the dial.
hinthintstring''Secondary hint text displayed under the label.
helperTexthelper-textstring''Helper text displayed below the dial.
errorMessageerror-messagestring''Error message shown under the dial (replaces the helper text). Setting it raises error; clearing it lowers it.
warningMessagewarning-messagestring''Warning message shown under the dial (replaces the helper text). Setting it raises warning; clearing it lowers it.
sizesize'small' | 'medium' | 'large''medium'Size variant affecting dial diameter.
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).
tickSnaptick-snapbooleanfalseWhen 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.
suffixsuffixstring'°'String appended to the value readout. The degree sign by default.
hideValuehide-valuebooleanfalseHides the numeric value readout beside the dial.
displayPrecisiondisplay-precisionnumber | undefined—Number of decimal places shown in the value readout and aria-valuetext. Leave unset to infer from step (step=0.5 → 1, step=1 → 0). Overridden by formatValue when set.
formatValue—(value: number) => string | undefined—Custom formatter for the value readout and aria-valuetext. When set, owns the full display string — suffix is NOT appended and displayPrecision is ignored. Must be a function, so it cannot be set via an HTML attribute; assign it as a property (el.formatValue = fn) or via a framework property binding (.formatValue=${fn} in Lit, formatValue={fn} in React).
localelocalestring | undefined—BCP-47 locale for the readout's digits and separators. Unset, the nearest lang (across shadow roots), else the browser's language. The value and the form value stay plain numbers.
inputIdinput-idstring''Explicit id for the underlying input; empty uses the fixed input (components.md "Ids").
autofocusautofocusbooleanfalseMarks 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 ​

EventDetailDescription
input—Fired during drag / keyboard adjustment. Detail: { value: number }
change—Fired when the value is committed (pointer release / blur). Detail: { value: number }

CSS Parts ​

PartDescription
baseThe outer wrapper (label + dial + helper text column)
labelThe label container (label + hint)
rowRow containing the dial and the value readout
valueThe numeric value readout beside the dial
dialThe clickable circle that owns the rim + indicator + labels
inputThe hidden native range input
rimThe visible circle border
indicatorThe line from center to the current angle
thumbThe thumb dot at the tip of the indicator
hubThe small disc at the center of the dial
tick-marksContainer for the rim tick lines (only present when tickMarks has entries)
tick-markIndividual tick line on the rim
tick-mark-labelsContainer for tick labels positioned around the rim
tick-mark-labelIndividual label outside the rim
helper-textThe helper text element
error-messageThe error message element
warning-messageThe warning message element
message-iconThe icon starting the error or warning message

CSS Custom Properties ​

The documented hooks. Every override hook the component reads — one per token it paints — is listed in @whitespaceux/harmony/theme-api.json, and @whitespaceux/harmony/component-bindings.json (the Harmony MCP get-bindings tool) says which part and state each one paints.

PropertyDefaultDescription
--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-border-default-restvar(--hy-border-default-rest)Rim stroke color
--hy-angle-picker-border-danger-restvar(--hy-border-danger-rest)Rim stroke color
--hy-angle-picker-border-warning-restvar(--hy-border-warning-rest)Rim stroke color
--hy-angle-picker-rim-stroke-mdvar(--hy-stroke-md)Rim stroke width
--hy-angle-picker-fill-colortransparentOptional fill behind the indicator (defaults to transparent)
--hy-angle-picker-background-brand-indicator-restvar(--hy-background-brand-indicator-rest)Color of the indicator line + hub
--hy-angle-picker-background-danger-indicator-restvar(--hy-background-danger-indicator-rest)Color of the indicator line + hub
--hy-angle-picker-background-warning-indicator-restvar(--hy-background-warning-indicator-rest)Color of the indicator line + hub
--hy-angle-picker-indicator-stroke-mdvar(--hy-stroke-md)Indicator stroke width
--hy-angle-picker-hub-ornament-bulletvar(--hy-ornament-bullet)Diameter of the center disc
--hy-angle-picker-hub-ornament-tickvar(--hy-ornament-tick)Diameter of the center disc
--hy-angle-picker-hub-ornament-handlevar(--hy-ornament-handle)Diameter of the center disc
--hy-angle-picker-thumb-ornament-handlevar(--hy-ornament-handle)Diameter of the draggable disc at the indicator tip
--hy-angle-picker-thumb-ornament-bulletvar(--hy-ornament-bullet)Diameter of the draggable disc at the indicator tip
--hy-angle-picker-thumb-ornament-pelletvar(--hy-ornament-pellet)Diameter of the draggable disc at the indicator tip
--hy-angle-picker-tick-foreground-subtle-restvar(--hy-foreground-subtle-rest)Color of rim tick lines (only when tickMarks is set)
--hy-angle-picker-tick-label-foreground-subtle-restvar(--hy-foreground-subtle-rest)Color of rim tick labels
--hy-angle-picker-value-foreground-subtle-restvar(--hy-foreground-subtle-rest)Value readout color
--hy-angle-picker-value-width—Minimum width of the value readout column (measured from the widest angle by default)
--hy-angle-picker-foreground-danger-restvar(--hy-foreground-danger-rest)Error message text and icon color
--hy-angle-picker-foreground-warning-restvar(--hy-foreground-warning-rest)Warning message text and icon color
--hy-angle-picker-gap-layout-2xsvar(--hy-gap-layout-2xs)Gap between the message icon and its text
--hy-angle-picker-label-radius—Radial distance from center to the nearest edge of each tick label. Default: dial radius + 0.25em
--hy-angle-picker-motion-transition-hover-durationvar(--hy-motion-transition-hover-duration)Transition duration for hover and focus state feedback
--hy-angle-picker-motion-transition-hover-easingvar(--hy-motion-transition-hover-easing)Transition easing for hover and focus state feedback
--hy-angle-picker-thumb-ring-color—Ring around the thumb (follows the indicator color)

Built with Lit. Documented with VitePress.