Rotary knob
FreeA rotary control with a real edge. The knob is a short cylinder seen from above: a stack of discs gives it thickness, a ring of ticks lights up to the value, and the number is always written beside it. Drag it or use the keys; with detents on it clicks through every tenth.
pnpm dlx shadcn@latest add @caliper/rotary-knobDrag, or use the arrow keys, Home and End.
When to use #
Use it for
- Gain, volume, brightness and other controls that feel like a physical dial.
- Settings where the number matters less than the feel of turning it.
Skip it when
- Precise entry; pair a slider with a number field.
- Forms that need a native control for autofill or validation.
Decisions #
Why it is built this way, so you know what to keep when you change your copy.
Behaviour
- Drag uses straight lines
- Right or up raises the value, left or down lowers it, so nobody has to circle the knob. It works the same on touch.
- It is a slider to assistive technology
- role slider with the value and a spoken value text. Arrows, PageUp, PageDown, Home and End all work, and Shift steps by a tenth.
Styling
- Ember marks the one thing that moves
- The notch on the cap is the only brand colour; the ticks light in the foreground colour.
Installation #
pnpm dlx shadcn@latest add @caliper/rotary-knob
Adds its 5 files to components/ui/, each with its own stylesheet, and installs their packages. No account needed. The first part also adds Caliper UI’s tokens; import them once as in Installation.
Usage #
This is the code behind the preview above.
import { useState } from "react" import { RotaryKnob } from "@/components/ui/rotary-knob" export function RotaryKnobDemo() { const [gain, setGain] = useState(42); return ( <div className="w-full max-w-sm"> <RotaryKnob label="Gain" unit="%" caption="% gain" value={gain} onValueChange={setGain} resetValue={50} valueText={(v) => `${v} percent gain`} /> </div> ); }
API reference #
RotaryKnob
component
Accessibility #
It exposes slider semantics, with aria-label, aria-valuemin, aria-valuemax, aria-valuenow, aria-valuetext and aria-describedby kept in step with state.
Motion #
It declares no animation in its component source. Any transition comes from its stylesheet, which uses the duration tokens that collapse to instant under prefers-reduced-motion.