DreamKnob/packages/dreamknob
Dreamodus 0aee48b749 Tier 2: drag feel, wheel overhaul, refs/meta/forms, Meter, gradients
- rotaryMode: 'absolute' | 'relative' (angle deltas, no grab jump) |
  'pickup' (MIDI-style: engages when the pointer sweeps past the value)
- dragAcceleration: velocity gain for relative drags (Shift bypasses);
  hideCursorOnDrag hides the cursor mid-gesture
- Wheel: trackpad delta accumulation (flicks can't rocket the value,
  one notch per wheel click), wheelStep override, wheelRequiresFocus
  opt-out of the hover scroll-trap
- onChange/onChangeStart/onChangeEnd now receive { source: 'drag' |
  'wheel' | 'keyboard' | 'reset' | 'api' }
- Every component forwards a ref to its root; name prop renders a
  hidden form input; SVG primitives carry data-part attributes
- Arc gradient: position-anchored color stops rendered as sliced arcs
  (mixColors/sampleGradient exported, unit-tested)
- New read-only Meter: LED level meter with peak hold, zones, readout
- Fader capColor/capLength for custom handle styling
- Docs: Meter/gradient/drag-feel gallery cards, white-cap fader demo,
  API rows, README
2026-07-12 15:18:54 -07:00
..
src Tier 2: drag feel, wheel overhaul, refs/meta/forms, Meter, gradients 2026-07-12 15:18:54 -07:00
package.json Initial release: dreamknob library + showcase 2026-07-12 14:26:52 -07:00
README.md Tier 2: drag feel, wheel overhaul, refs/meta/forms, Meter, gradients 2026-07-12 15:18:54 -07:00
tsconfig.json Initial release: dreamknob library + showcase 2026-07-12 14:26:52 -07:00

dreamknob

Studio-grade React knobs & faders. Rotary, linear, 2D, 3D, digital and analog — with a headless core, any number range, any decimal precision, and colors you fully control. Zero runtime dependencies.

npm install dreamknob

Quick start

import { FlatKnob } from 'dreamknob'

function Volume() {
  const [value, setValue] = useState(64)
  return (
    <FlatKnob
      size={90}
      value={value}
      onChange={setValue}
      min={0}
      max={100}
      step={0.5}
      label="Volume"
      unit="%"
      aria-label="Volume"
    />
  )
}

Uncontrolled works too: pass defaultValue instead of value/onChange (it also becomes the double-click reset target).

Interaction model

Every component shares one core:

Gesture Behaviour
Drag rotary (grab & turn, follows the pointer angle), vertical, horizontal, both, or absolute track-* (faders)
Scroll wheel one step per notch, Shift = fine
↑ → / ↓ ← ± one step (Shift = 10×)
PageUp/PageDown ± 10 % of range
Home/End min / max
Double-click reset to defaultValue — or open the type-in editor with editable
Escape cancel an in-flight drag, restoring the start value
Touch / pen pointer capture, touch-action: none

All knobs render role="slider" with aria-valuemin/max/now/text.

Value handling

  • min / max — any numbers (negative, fractional, huge).
  • step — decimal snap increments (0.5, 0.001, 1e-7…) with float-drift-safe rounding; display precision inferred automatically, overridable via decimals.
  • values={[0.25, 0.5, 1, 2, 4]} — discrete allowed values.
  • steps={5} — evenly spaced detents (selector knobs).
  • taperlinearTaper (default), logTaper (frequencies), powTaper(n) (gain), or your own { toNormalized, fromNormalized }.
  • origin={0} — bipolar anchor: arcs/fader fills draw from this value (pan, gain trim).
  • detents={[0]} + detentSize — magnetic snap points while dragging.
  • editable — double-click (knobs) or click the readout (faders) to type an exact value; parseValue customizes parsing ("1.2k" → 1200 out of the box).
  • rotaryMode'absolute' (rc-knob feel), 'relative' (no grab jump), or 'pickup' (MIDI-style: engages when the pointer sweeps past the value).
  • dragAcceleration — velocity gain for relative drags; hideCursorOnDrag hides the cursor mid-gesture.
  • wheelStep / wheelRequiresFocus — tune wheel notches, or keep hover-scroll free.
  • onChange / onChangeStart / onChangeEnd — gesture-aware callbacks with meta.source ('drag' | 'wheel' | 'keyboard' | 'reset' | 'api').
  • name — hidden form input for plain <form> posts; every component forwards a ref to its root element, and SVG parts carry data-part attributes for CSS.

Prebuilt skins

import {
  FlatKnob,     // clean 2D arc knob (arcFrom="center" for pan knobs)
  MetalKnob,    // 3D brushed aluminum, knurled rim (tone="silver" | "dark")
  RubberKnob,   // 3D soft-touch synth knob with glow halo
  VintageKnob,  // chicken-head bakelite over a printed scale
  LEDKnob,      // segmented LED ring + true seven-segment readout
  NeonKnob,     // glowing arc for dark UIs
  SteppedKnob,  // detented selector (positions={['LP','BP','HP']})
  Fader,        // linear channel fader, vertical or horizontal
  LEDFader,     // segmented LED meter-fader with color zones
  ImageKnob,    // film-strip sprite knob (KnobMan-style PNG strips)
  Meter,        // read-only LED level meter with peak hold
  SegmentDisplay, // standalone seven-segment numeric display
} from 'dreamknob'

Theming

import { DreamknobProvider } from 'dreamknob'

<DreamknobProvider base="dark" theme={{ accent: '#ff4d6b' }}>
  {/* every control below inherits the tokens; instance props still win */}
</DreamknobProvider>

base="light" swaps in light-background defaults. Tokens: accent, track, face, text, label, ticks, focusRing, fontMono, fontUI.

Every skin takes the core props plus size, label, showValue, unit, format, and per-part color props (color, trackColor, faceColor, bodyColor, …).

Compose your own

import { Knob, Arc, Pointer, Ticks, KnobValue } from 'dreamknob'

<Knob size={96} min={-24} max={24} step={0.5} defaultValue={0} aria-label="Trim">
  <Ticks count={25} activeColor="#ffd23e" />
  <Arc radius={36} thickness={3} color="#ffd23e" from="center" />
  <Pointer type="triangle" radius={33} length={9} width={8} color="#ffd23e" />
  <KnobValue unit=" dB" />
</Knob>

<Knob> also accepts a render function: {(ctx) => <YourSVG angle={ctx.angle} />}.

Fully headless

import { useKnob } from 'dreamknob'

const { value, normalized, angle, isDragging, bind, setValue } = useKnob({
  min: 0, max: 11, step: 0.1, defaultValue: 11, 'aria-label': 'Volume',
})
return <div {...bind}>your own rendering</div>

MIT