Scrub input

A Figma-style number field: drag the label to scrub, or type values, units, and arithmetic.

pnpm dlx shadcn@latest add @uiarc/scrub-input
Live · keyboard ready
  • Position, size, rotation, and opacity fields in an inspector panel.
  • Dense settings where typing exact values and dragging both matter.
  • Values with units such as px, %, or deg that people type mixed.
  • Use number-field for a normal form with stepper buttons and a visible label.
  • Use slider when the range and position matter more than the exact number.
  • Use money-input for currency amounts.

Installation

Add Scrub input with the shadcn CLI, or copy the source by hand.

pnpm dlx shadcn@latest add @uiarc/scrub-input

Adds the component and its local dependencies, and installs motion. First time? Add the @uiarc registry to components.json, or use the full URL:

example.tsx
import { useState } from "react";import { ScrubInput } from "@/components/arc/scrub-input/scrub-input"; export function LayerInspector() {  const [width, setWidth] = useState(240);  const [opacity, setOpacity] = useState(100);  return (    <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 8 }}>      <ScrubInput label="W" value={width} onValueChange={setWidth} min={0} units={["px", "%"]} resetValue={240} />      <ScrubInput label="Opacity" value={opacity} onValueChange={setOpacity} min={0} max={100} unit="%" />    </div>  );}

Rotation that wraps

example.tsx
<ScrubInput label="R" min={0} max={360} wrap unit="°" units={["°"]} fineStep={0.1} onValueCommit={value => setRotation(value)} />

API reference

2 parts. The first is the root.

ScrubInput

A compact number field for inspectors: drag the label to scrub, or type values with arithmetic and units, with rubber-band limits.

PropTypeDefaultDescription
labelRequiredReactNode–Text or a 16px icon. It is the scrub handle. Icon labels need aria-label.
valuenumber–Controlled value.
defaultValuenumber0Starting value when uncontrolled.
onValueChange(value: number, details: ScrubInputChangeDetails) => void–Fires for every change, including each scrub step. details is { source: "scrub" | "type" | "key" | "reset", unit }.
onValueCommit(value: number, details: ScrubInputChangeDetails) => void–Fires once when a change settles: a scrub ends, Enter or blur commits typing, or a key step lands.
minnumber-InfinityLower limit.
maxnumberInfinityUpper limit.
stepnumber1One step for arrows and scrubbing.
fineStepnumberstep / 10Step with Alt held.
coarseStepnumberstep * 10Step with Shift held. Coarse steps snap to the coarse grid.
precisionnumber–Decimal places kept. Defaults to the precision of step or fineStep.
pixelsPerStepnumber2Pointer travel in px for one step while scrubbing.
wrapbooleanfalseWraps from max to min, as rotation does. Needs both limits.
unitstring–Controlled unit shown after the number. With more than one entry in units it becomes a button that cycles them.
defaultUnitstring–Starting unit when uncontrolled. Defaults to unit or the first of units.
unitsreadonly string[]–Units a typed value may carry, such as ["px", "%"]. Defaults to the current unit.
onUnitChange(unit: string) => void–Called when a typed unit or the unit button changes the unit.
resetValuenumber–Double click the label to return here.
trackbooleantrue when min and max are setShows where the value sits between min and max as a quiet fill.
size"sm" | "md""md"Field height: 30px, or the small control height.
pointerLockbooleantrueLocks the mouse pointer while scrubbing when the browser allows it.
classNamestring–Class on the root.
...propsInputHTMLAttributes<HTMLInputElement>–Forwarded to the input, except value, defaultValue, onChange, type, size, min, max, step, and prefix.
refRef<HTMLInputElement>–The input element.

evaluateScrubExpression

Evaluates + - * / and parentheses over numbers that may carry a unit. Returns { value, unit? }, or null for anything else or an unknown unit.

PropTypeDefaultDescription
textRequiredstring–The typed expression, such as "(100-24)/2" or "50%". Commas read as decimal points.
unitsRequiredreadonly string[]–Allowed units. deg and ° are aliases.
ArrowUporArrowDown
Steps the value; Shift steps ten times coarser, Alt ten times finer.
Enter
Commits typed text, including arithmetic such as 12*2.
Escape
Restores the last value while typing, or cancels a scrub back to where it started.
ShiftorAlt (while scrubbing)
Switches to coarse or fine steps mid-drag and shows the multiplier.
  • The input is role="spinbutton" with aria-valuenow, finite aria-valuemin and aria-valuemax, and aria-valuetext that spells out percent and degrees.
  • The label is a real label for the input; clicking it focuses and selects the value.
  • Invalid typed text sets aria-invalid for 900ms and keeps the previous value.
  • The unit cycle button is labelled Unit: <unit>. Change unit.
  • Past min or max the field strains with rubber-band resistance, up to 12px, and springs back on release; the number leans with it.
  • The track fill follows a drag one to one and glides to typed or stepped values.
  • Invalid input and steps against a limit kick the field with a short shake; unit changes rise from a small blur.
  • Reduced motion removes the strain, shake, and glide, and swaps the unit with a fade.
  • The field fills its column; set --scrub-input-width to size it.
  • Touch scrubs through pointer capture on the label, with touch-action: pan-y so vertical page scroll still works; pointer lock is mouse only.
  • With the pointer locked, a stand-in cursor portaled to the body wraps at the window edges so a long drag never runs out of screen.
  • Scrubbing writes motion values directly and emits only when the rounded value changes.
  • One ResizeObserver per field tracks its width for the strain scale.

Notes for AI

Give your coding assistant the Markdown reference instead of screenshots.

  • Use in design tool inspectors, property panels, and editors where people tune values by feel.
  • Save on onValueCommit, not onValueChange, which fires on every scrub step.
  • Set min and max for bounded values to get the track fill; set wrap for angles.
  • For a labelled form field with plus and minus buttons use number-field; for a range with a thumb use slider.

The full library index for assistants is at /llms.txt.