I was working on an interactive graph component for an upcoming article when I hit a frustrating UX bug. The component has a range input at the bottom — a timeline scrubber for controlling animation progress. When dragging the thumb from left to right, if I moved even slightly too fast, the pointer would escape the input's hit area. The whole thing would freeze mid-drag.
At first it felt like lag, but the real issue was simpler: the capture area on a native <input type="range"> is tiny. Move the pointer outside it and the browser just stops tracking.
The fix was two parts. First, call setPointerCapture on pointer down — this locks all pointer events to the element, so even when the cursor leaves, it keeps tracking. Second, add the touch-none CSS class to prevent the browser from hijacking pointer events for scroll or pan gestures on touch devices. Together they made the slider feel completely solid, no matter how fast you drag.
But that introduced a subtler bug in Chrome. The slider had a "complete" state that triggered when progress hit exactly 1 — showing a replay button. With setPointerCapture, dragging to the very end looked visually complete but never actually triggered completion. The value would land at 0.999... instead of 1.
The issue: there was no pointerup handler. The last position update came from pointermove, which in Chrome fires a frame or two before the actual release. The final cursor position — the one that would push the value to 1 — arrived via pointerup, and nobody was listening. Safari happened to fire a final pointermove at the release point, masking the problem.
The full fix is three handlers, not two: pointerdown to capture and set the initial position, pointermove to track during the drag, and pointerup to catch the exact release position.