Dial Knob
A rotary knob input: an SVG tick ring, a subtle nudge at detents, a spring-settled pointer and a rolling readout, with wheel and arrow-key support.
Adjust a value the way you would on a hardware knob. Press and drag around the knob (or switch to up/down dragging) and the SVG tick ring and value arc track the position live; the pointer follows the target angle on a spring, so fast turns lag slightly and settle into place. With detents set, dragging near a detent clicks into it with a small nudge; the readout below rolls digit by digit and can carry a unit. The mouse wheel and full keyboard control are supported, in both controlled and uncontrolled modes.
Bring this into your project
Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.
npx webberui@latest add dial-knob.enPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<DialKnob/>
The complete example includes the preview layout and data. Install the component first, then paste this file into your project.
Installation
npx webberui@latest add https://webberui.com/r/dial-knob.en.jsonOr, once registries are configured in components.json, install it as @webberui/dial-knob.
Usage
import { DialKnob } from "@/components/ui/dial-knob";
const [temp, setTemp] = React.useState(26);
<DialKnob
label="Temperature"
value={temp}
onValueChange={setTemp}
onValueCommit={(v) => console.log(`Send ${v}°C`)}
min={16}
max={30}
step={0.5}
detents={[24]}
unit="°C"
formatValue={(v) => v.toFixed(1)}
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | — | Knob name, used as aria-label and shown under the readout (required) |
value | number | — | Controlled value; managed internally when omitted |
defaultValue | number | min | Initial value in uncontrolled mode |
onValueChange | (value: number) => void | — | Called when the value changes; fires continuously while dragging |
onValueCommit | (value: number) => void | — | Called on drag release, after a key press, or ~200ms after the wheel stops |
min | number | 0 | Minimum value |
max | number | 100 | Maximum value |
step | number | 1 | Increment per step; values are quantized to it |
detents | number[] | — | Detents, e.g. [0, 50, 100] |
snapThreshold | number | 3% of range | How close a drag must get to a detent before it snaps |
arc | number | 270 | Total rotation range in degrees, symmetric around 12 o'clock |
dragMode | "angle" | "vertical" | "angle" | Turn around the center, or drag up and down (200px covers the full range) |
ticks | number | 27 | Number of ticks on the outer ring; every 5th tick is long |
size | "sm" | "md" | "lg" | number | "md" | 64 / 96 / 140 px, or a pixel number |
unit | string | — | Readout suffix, e.g. "%", "°C" |
formatValue | (value: number) => string | — | Custom readout format, also used for aria-valuetext; defaults to the decimal places of step |
stiffness | number | 320 | Pointer spring stiffness (damping fixed at 22); higher settles faster |
disabled | boolean | false | Disable all interaction |
className | string | — | Passed to the outermost container |
How it works
- Angle mapping: the value maps linearly from
-arc/2to+arc/2(0° is straight up); the tick ring, value arc and pointer all share that mapping. - Angle dragging accumulates the change in pointer angle around the center, wrapping each delta into ±180°, so passing 6 o'clock never jumps the value; turning back after overshooting the range responds immediately.
- Snapping only applies to dragging: an unsnapped continuous value is kept internally, so moving past the threshold releases the detent. Keys and the wheel move one
stepat a time so they never get pulled back into a detent; whenever the value lands on a detent, the knob body does a 120ms wiggle, and phones that support it vibrate for 8ms. - The wheel listener is bound natively with
passive: falseand only blocks page scrolling while the pointer is over the knob; small trackpad deltas accumulate into one notch. - The accent comes from the
--wb-dial-accentvariable (neutral-900 in light, neutral-100 in dark), shared by the value arc and pointer line; transitions use--wb-duration-fastand--wb-ease-out.
Accessibility
- The knob itself is
role="slider"withtabIndex=0,aria-valuemin,aria-valuemaxandaria-valuenow;aria-valuetextis the formatted value plus unit (e.g. "24.0°C") andaria-labelcomes fromlabel - Keyboard: ← / ↓ step down, → / ↑ step up, PageUp / PageDown move 10 steps, Home / End jump to min / max
- A
focus-visiblering marks focus; whendisabled,aria-disabledis set and the knob leaves the tab order - The visible readout and name are hidden from assistive tech to avoid double announcements
- When the user's system has reduce motion turned on, the pointer jumps straight into place (no spring), there is no detent nudge, and the readout does not roll
Combobox
A type-to-filter picker that never filters or selects mid-composition for CJK input, with an anchored list that flips at edges, a sliding highlight, and removable chips in multi-select mode.
Switch-to-Slider Morph
A React switch and slider that share one thumb: turning it on makes the thumb leap onto a vertical or horizontal slider to set a value, turning it off sends it back. Built for enable-then-adjust settings.