Angle Picker
Full-circle angle input (0–359°) with wrapping keyboard steps and pointer rotation.
| Package | @vizejs/ui/angle-picker |
| Maturity | stable |
| Own the source | vize lib pull angle-picker |
| Requires | controllable-state, id, knob |
| Aliases | angle input, rotation picker, hue wheel, direction picker |
| Covers | WAI-ARIA Slider, Figma angle input, Mantine AngleSlider |
Minimal setup
Use a Vue 3.5+ project with Vue SFC compilation configured (see Vite+ integration).
vp install @vizejs/ui
Add the public import below to <script setup lang="ts">. Components provide behavior without styles; the preview uses optional Paper styles.
import { AnglePicker } from "@vizejs/ui/angle-picker";
Or copy the source into your project with vize lib pull angle-picker (see Source Distribution).
Try it
This is the basic example maintained with the component source, using the public package imports. The preview adds the optional Paper styles.
Open the interactive example · Styling these components
Copy the example
<!-- Controlled angle picker labelled by visible text, with the angle exposed to its slot. -->
<script setup lang="ts">
import { ref, useId } from "vue";
import { AnglePicker } from "@vizejs/ui/angle-picker";
const direction = ref(90);
const labelId = useId();
</script>
<template>
<div>
<span :id="labelId">Gradient direction</span>
<AnglePicker
v-slot="{ angle }"
v-model="direction"
name="direction"
:large-step="45"
:aria-labelledby="labelId"
>
<span aria-hidden="true">{{ angle }}°</span>
</AnglePicker>
<output>{{ direction }} degrees</output>
</div>
</template>
API
AnglePicker
Source: src/families/form/angle-picker/angle-picker.vue
Props
| Prop | Type | Default | Description |
|---|---|---|---|
id |
string | null |
undefined |
Element id. null and undefined select a deterministic fallback. |
name |
string |
undefined |
Native form field name; a hidden input submits the angle in degrees. |
modelValue |
number |
undefined |
Controlled angle in degrees; any number is wrapped into [0, 360). |
defaultValue |
number |
0 |
Initial uncontrolled angle, also restored by form reset. |
step |
number |
1 |
Angle step in degrees. |
largeStep |
number |
15 |
Angle step for Page Up and Page Down. |
disabled |
boolean |
false |
Disable interaction and form submission. |
readOnly |
boolean |
false |
Keep focus and form value while preventing user changes. |
getValueText |
(angle: number) => string |
(angle) => |
Formats aria-valuetext. |
ariaLabel |
string |
undefined |
Accessible name when no label or aria-labelledby supplies one. |
ariaLabelledby |
string |
undefined |
Ids that label the picker. |
ariaDescribedby |
string |
undefined |
Ids that describe the picker. |
ariaErrormessage |
string |
undefined |
Id of the validation error message used while invalid. |
ariaInvalid |
AnglePickerAriaInvalid |
false |
Invalid state announced to assistive technology. |
Events
| Event | Payload | Description |
|---|---|---|
update:modelValue |
[angle: number] |
Fired for every requested angle, including drag frames. |
change |
[angle: number, source: AnglePickerChangeSource] |
Fired once a change is committed (key press, pointer release, or API). |
Slots
| Slot | Slot props | Description |
|---|---|---|
default |
AnglePickerSlotState |
Renders the dial and indicator with the current angle. |
Exposed
| Member | Type | Description |
|---|---|---|
value |
number |
Current value. |
angle |
number |
Indicator angle in degrees (clockwise from 12 o'clock). |
percent |
number |
Position within the bounds, 0–100. |
min |
number |
Normalized lower bound. |
max |
number |
Normalized upper bound. |
dragging |
boolean |
Whether a pointer drag is active. |
disabled |
boolean |
Whether interaction is disabled. |
state |
KnobState |
Stable state token. |
element |
HTMLSpanElement | null |
Rendered role="slider" element. |
focus |
(options?: FocusOptions) => void |
Focus the rotary element. |
setValue |
(value: number) => boolean |
Request a value (snapped/clamped or wrapped); returns whether it changed. |
reset |
() => boolean |
Restore the default value; returns whether it changed. |
Behavior
Normative state x input -> outcome table for angle-picker.vue
(@vizejs/ui/angle-picker), a full-circle angle input with WAI-ARIA APG slider
semantics that reuses Knob's rotary geometry and drag handling. Every row is
proven by the named test in angle-picker.test.ts or ../knob/knob-ssr.test.ts;
compile-only assertions live in angle-picker.types.test-d.ts.
| # | State | Input | Outcome | Proven by |
|---|---|---|---|---|
| A1 | seeded | render | role="slider" with 0 to 360 - step, wrapped value, "N degrees" value text, --vize-angle-picker-angle, hidden value |
renders a full-circle slider with degree value text and a CSS angle |
| A2 | focused | Arrow / Page / Home / End | steps wrap around the circle; Home is 0°, End is the last step; each change commits | keyboard steps wrap around the circle |
| A3 | idle | pointer drag | the pointer angle around the center becomes the value, snapped to step; release commits once |
pointer rotation follows the pointer and snaps to the step |
| A4 | controlled / disabled / API | keys, expose | controlled values win; disabled pickers ignore keys; setValue wraps; reset restores the default |
controlled, disabled, and imperative behavior |
| A5 | SSR | isolated requests | byte-identical markup and hydration without diagnostics | renders byte-identical rotary markup and hydrates without mismatches |
The subpath is tree-shakable and ships no CSS; those package contracts are
pinned by distribution.test.ts, check:size, and check:tree-shaking.