Knob
Rotary knob/dial: APG slider semantics with angle-mapped pointer dragging.
| Package | @vizejs/ui/knob |
| Maturity | stable |
| Own the source | vize lib pull knob |
| Requires | controllable-state, id |
| Aliases | knob, dial, rotary input, potentiometer |
| Covers | WAI-ARIA Slider, React Aria useSlider (rotary), Web Audio UI knobs |
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 { Knob } from "@vizejs/ui/knob";
Or copy the source into your project with vize lib pull knob (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
<!-- Volume knob bound with v-model, labelled by visible text and announcing a percentage. -->
<script setup lang="ts">
import { ref, useId } from "vue";
import { Knob } from "@vizejs/ui/knob";
const volume = ref(40);
const labelId = useId();
</script>
<template>
<div>
<span :id="labelId">Master volume</span>
<Knob
v-slot="{ value }"
v-model="volume"
name="volume"
:min="0"
:max="100"
:aria-labelledby="labelId"
:get-value-text="(value: number) => `${value}%`"
>
<span aria-hidden="true">{{ value }}%</span>
</Knob>
</div>
</template>
API
Knob
Source: src/families/form/knob/knob.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 value. |
modelValue |
number |
undefined |
Controlled value. undefined selects uncontrolled behavior. |
defaultValue |
number |
min |
Initial uncontrolled value, also restored by form reset. |
min |
number |
0 |
Lower bound. |
max |
number |
100 |
Upper bound (repaired to min + 1 when not greater than min). |
step |
number |
1 |
Positive step. |
largeStep |
number |
step * 10 |
Positive step for Page Up and Page Down. |
startAngle |
number |
-135 |
Indicator angle of min, in degrees clockwise from 12 o'clock. |
endAngle |
number |
135 |
Indicator angle of max; the sweep must be positive and at most 360°. |
allowWheel |
boolean |
false |
Opt in to wheel stepping while focused. |
disabled |
boolean |
false |
Disable interaction and form submission. |
readOnly |
boolean |
false |
Keep focus and form value while preventing user changes. |
getValueText |
(value: number) => string |
undefined |
Formats aria-valuetext, for example (v) => \${v} dB``. |
ariaLabel |
string |
undefined |
Accessible name when no label or aria-labelledby supplies one. |
ariaLabelledby |
string |
undefined |
Ids that label the knob. |
ariaDescribedby |
string |
undefined |
Ids that describe the knob. |
ariaErrormessage |
string |
undefined |
Id of the validation error message used while invalid. |
ariaInvalid |
KnobAriaInvalid |
false |
Invalid state announced to assistive technology. |
Events
| Event | Payload | Description |
|---|---|---|
update:modelValue |
[value: number] |
Fired for every requested value, including drag frames. |
change |
[value: number, source: KnobChangeSource] |
Fired once a change is committed (key press, pointer release, wheel, or API). |
Slots
| Slot | Slot props | Description |
|---|---|---|
default |
KnobSlotState |
Renders the dial face and indicator with the angle and value. |
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 knob.vue (@vizejs/ui/knob), a
rotary control with WAI-ARIA APG slider semantics. Angles are degrees clockwise
from 12 o'clock over a sweep (default -135° to 135°). Every row is proven by the
named test in knob.test.ts or knob-ssr.test.ts; compile-only assertions live
in knob.types.test-d.ts.
| # | State | Input | Outcome | Proven by |
|---|---|---|---|---|
| K1 | geometry | angle <-> value | values map linearly onto the sweep; pointer angles in the dead zone resolve to the nearer end (no jump across ends) | maps angles and values with a dead zone that never jumps across ends |
| K2 | named | render | focusable role="slider" with aria-valuenow/min/max/valuetext, --vize-knob-angle/--vize-knob-percent, hidden value |
renders an APG slider with angle hooks and a hidden form value |
| K3 | focused | Arrow / Page / Home / End | Up/Right increase, Down/Left decrease, pages use largeStep, Home/End jump to bounds; each distinct change commits |
keyboard steps, pages, and jumps to bounds, committing each change |
| K4 | idle | pointer drag | press focuses and maps the pointer angle around the center; moves update the value; release commits once | dragging rotates around the center and commits once on release |
| K5 | wheel / locked | wheel, keys, pointer | wheel is opt-in and needs focus; disabled and read-only knobs ignore input and emit nothing | wheel is opt-in and needs focus; disabled and read-only knobs ignore input |
| K6 | controlled / form | keys, reset, API | controlled values win; the hidden input submits and form reset restores defaultValue; setValue snaps; reset, focus |
controlled values, form reset, and the imperative API |
| K7 | SSR / hydration | isolated requests | byte-identical markup with the CSS angle and no NaN/Infinity; hydration without diagnostics |
renders byte-identical rotary markup and hydrates without mismatches |
Public extension contract
| Surface | Contract |
|---|---|
| Parts | root (the role="slider" element; the slot draws the face). |
| Data attributes | data-vize-ui="knob", data-state (idle/dragging/readonly/disabled). |
| CSS custom properties | --vize-knob-angle (deg) and --vize-knob-percent. |
The subpath is tree-shakable and ships no CSS; those package contracts are
pinned by distribution.test.ts, check:size, and check:tree-shaking.