Skip to content
Lauren DormanDesign Engineer

Studies

Scrub field

Dragging a number by its label without losing the click, the cursor or where you started.

Published
Oct 14, 2026
Built with
React, Pointer Events, Pointer Lock, StyleX

Hamburgefonstiv 0123456789

Sphinx of black quartz, judge my vow.

Drag any label sideways to scrub its value. Shift moves ten times faster, Option (Alt) ten times slower. Or click into the number and type: arithmetic works, so 48/3, 12*2 and (10+4)*2 all resolve when you press Enter.

The specimen is set in this site’s typeface, Inter, which has a weight axis and an optical-size axis. Optical size follows the font size until you touch it, then holds whatever you set. The Auto toggle hands it back.

Details

  • A click is still a click. Scrubbing starts after 3px of sideways travel. A step is only 2px, so without that margin a wobble during a click would nudge the number. A plain click just focuses the field and puts the caret where you clicked.
  • Unbounded travel. Once a mouse drag starts, the label requests pointer lock, so the edge of the screen never cuts a scrub short. The browser hides a locked pointer, which would leave you dragging blind. So a drawn resize cursor picks up where it vanished and wraps around at the edge of the window. (Without the lock, it falls back to pointer capture.)
  • Cancel means cancel. If the browser takes the gesture (a vertical swipe on touch, say), the value returns to where it started. That swipe was meant for the page, so I’d rather drop the scrub than leave a number you didn’t mean to set.
  • Keyboard parity. Arrows move one step, with the same modifiers as the drag (Shift ×10, Option ×0.1). Page Up and Page Down move ten steps, and Home and End jump to the limits. Enter commits what you typed, and Escape throws it away without leaving the field.

Accessibility

Each field is a labeled spinbutton with its range. The unit beside the number is hidden from assistive tech, so where a field has a unit, the spoken value includes it instead: “40 pixels”, not just a bare number.

How it’s built

  • Renders on change, not on move. The raw drag value lives in a closure, and React only hears about it when the rounded value changes. In a quick drag that’s still most moves. It matters more with Option held, when a step takes 20px.
  • Typed math, no eval. A small recursive-descent parser handles + - * / and parentheses, along with the typographic minus, × and ÷. It accepts the field’s own unit at the end (24px) and reverts on anything else. That’s more code than eval, but I think a field should know exactly what it accepts.

Not yet verified

Pointer-lock behavior in Safari and Firefox. Alt in place of Option on Windows and Linux. Scrubbing and typing on real touch devices. Spoken output in VoiceOver, NVDA and JAWS.