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
| PROP | PAYLOAD | NOTES |
|---|---|---|
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:
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.
<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).
<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.
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, whichrender()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.