VSTDOCSv0.0.34

DOCS / UI REFERENCE

Events & gestures

The RootView hit-tests every mouse event into the shadow tree with DOM-style bubbling, maintains hover chains, and applies hover:/active: style layers natively — no JS round-trip for a hover repaint.

Pointer events

PROPPAYLOADNOTES
onClick—Fires on release over the pressed node.
onDoubleClick—The DAW reset gesture — built-in controls use it for reset-to-default.
onWheel{dy, dx}Controls get first refusal on the wheel; scroll containers keep it otherwise. dy is the notch fraction (~0.1/notch); dx is nonzero on trackpads and tilt wheels.
onMouseEnter / onMouseLeave—Hover chain, parent-to-child, like the DOM.
onMouseDown / onMouseUp—Drive active: styles.
onDragStart / onDrag / onDragEnd{dx, dy, x, y}Deltas from the drag origin plus the live pointer position, in root coordinates — everything a knob or an XY pad needs.

Drag gestures

The built-in knob maps vertical drag to value with dragToValue(startValue, dy). Rolling your own control is a few lines:

a custom vertical fader
function Fader({ value, onChange }) {
  const start = useRef(0);

  return (
    <View
      className="w-10 h-40 bg-zinc-900 rounded-lg cursor-pointer"
      onDragStart={() => { start.current = value; }}
      onDrag={(e) => onChange(clamp01(start.current - e.dy / 160))}
    >
      <View
        className="absolute left-0 right-0 bottom-0 bg-lime-400 rounded-lg"
        style={{ height: `${value * 100}%` }}
      />
    </View>
  );
}

Keyboard & focus (since 0.0.20)

Declaring onKeyDown (or onFocus/onBlur, or a focus: style variant) makes a View focusable: clicking it focuses it, Tab/Shift-Tab cycle focusables in tree order, clicking empty space blurs. Keys arrive with web KeyboardEvent.key names ("ArrowUp", "Enter", "Escape", " ", "a") plus shift/ctrl/alt/meta flags. Unhandled keys fall through to the host.

arrow keys nudge, exactly like the web version
<View
  className="cursor-pointer focus:border-lime-400 focus:border-2 rounded-lg"
  onKeyDown={(e) => {
    if (e.key === "ArrowUp") onChange(clamp01(value + (e.shift ? 0.001 : 0.01)));
    if (e.key === "ArrowDown") onChange(clamp01(value - (e.shift ? 0.001 : 0.01)));
  }}
/>

The built-in Knob, Slider, Toggle, and XYPad (and their Param* twins) are keyboard-accessible out of the box — arrows ±1% (Shift = ±0.1% fine), PageUp/Down ±10%, Home/End to the stops, and Enter/Space flips a Toggle — the sliderKeyTarget helper is exported if you want the same model on a custom control. Focus is invisible by default; opt into a ring with a focus: variant. onKeyUp (since 0.0.22) fires when a key the node saw go down is released — key-repeat arrives as repeated onKeyDowns, exactly like the web. TextInput joins the Tab order too (since 0.0.23): Tab into it types, Tab leaves it and continues the cycle. And pointer-events-none makes a decorative overlay click-through — the node and its whole subtree stop receiving input.

Scroll containers

Give a View overflow-auto (or overflow-y-scroll / overflow-x-scroll) and a bounded size: children lay out at full size, the mouse wheel scrolls, the painter clips and draws thumbs. Both axes work — an axis engages when its content actually overflows. Trackpads scroll horizontally with their real deltaX; mice map Shift+wheel to horizontal, like the web. Set scrollTop / scrollLeft to reset the offsets programmatically (say, when switching tabs).

TSX
<View className="flex-1 overflow-y-scroll gap-2 p-3" scrollTop={0}>
  {items.map((item) => <Row key={item.id} item={item} />)}
</View>

<View className="h-24 overflow-x-scroll flex-row gap-2" scrollLeft={0}>
  {steps.map((step) => <StepCell key={step.id} step={step} />)}
</View>

Layout feedback — onLayout

Layout happens in C++ (Yoga), so JS normally never knows where anything landed. onLayout closes the loop: it fires with the node’s root-space rect (scroll-adjusted) whenever layout moves or resizes it — and only when the rect actually changes. This is the foundation for menus, tooltips, and popovers.

TSX
import { useLayoutRect, useOverlay } from "@vsreact/core";

function InfoTip({ children }) {
  const [rect, onLayout] = useLayoutRect();
  const overlay = useOverlay();

  return (
    <View
      onLayout={onLayout}
      onMouseEnter={() => rect && overlay.show(
        <View className="absolute rounded-lg bg-zinc-800 px-3 py-2"
              style={{ left: rect.x, top: rect.y + rect.height + 4 }}>
          <Text className="text-[11]">{children}</Text>
        </View>
      )}
      onMouseLeave={() => overlay.hide()}
    >
      <Text className="text-faint">?</Text>
    </View>
  );
}
  • useLayoutRect() — [rect, onLayout] sugar for capturing the rect in state.
  • useOverlay() — a slot in the overlay layer, which render() mounts after your app automatically so overlay content paints on top and receives input first. show(node) / hide(); unmount cleans up.
  • The built-in <Select> is built entirely from these parts.

Cursors

Cursors come from classes — cursor-pointer, cursor-text, cursor-default — applied per-node by the hit-tester as the mouse moves.