Scrub Number Field
A numeric input whose label you drag to scrub, with pointer lock and precision modifiers.
DOM + CSS1 dep1 file · 5 KB TSX
Props
"use client";
import { useCallback, useRef, useState } from "react";
import { MoveHorizontal } from "lucide-react";
export function ScrubNumberField({
label = "Blur radius",
step = 1,
suffix = "px",
min = 0,
max = 400,
defaultValue = 24,
onCommit,
}: {
label?: string;
step?: number;
suffix?: string;
min?: number;
// ---------------------------------------------------------------
// Full source available with a viberdy Pro subscription.
// https://viberdy.dev/pricing
// ---------------------------------------------------------------
}Details
About this component
Design tools have had scrubbable number fields for decades and the web mostly still has spinners with four-pixel arrows. The mechanism is pointer lock: on pointerdown the cursor is hidden and movement deltas keep arriving after the pointer would have run off the screen, so a single gesture can cover the whole range. Response is deliberately non-linear, so small movements are precise and committed ones are fast. Three details separate this from a naive implementation. Movement under pointer lock is unfiltered and on some Windows acceleration profiles arrives in bursts, so every event is clamped before use — without that a twitch can throw the value hundreds of units. Pointer lock can also simply be refused, since it needs a genuine gesture and is disallowed in several contexts, which makes the unlocked delta path mandatory rather than a fallback nicety. And the live value never touches React state during the drag: it is written to a ref and into the DOM directly, with a single commit on release, because a state update per pointermove re-renders on every pixel of travel. The field itself stays an ordinary text input, so typing, selection and screen-reader access all still work.
Curator’s note
The ±60px movement clamp is the line between a professional scrubber and one that occasionally throws the value across the range.
