lab

Drum

03 · aug 2026

A wheel that lands where you threw it, not where you let go. The four numbers underneath are yours to drag around.

3
4
5
6
7
8
9
10
11
12
34
35
36
37
38
39
40
41
42
43
44
45
46

drag, flick, or tap a value

0.998
320ms
0.55
1.00

Thrown, not scrolled

When you let go, the wheel doesn’t stop where your finger was. It works out where the throw was heading — (v / 1000) × g / (1 − g), the same exponential decay a scroll view uses — and springs to the value nearest that. It’s the difference between a control that received your gesture and one that merely recorded where it ended.

The whole feel of it lives in glide, and the range is smaller than it looks. At 0.990 a hard flick travels three values; at 0.999, thirty-two. Nine thousandths, a factor of ten, because the projection divides by what’s left of one. Then snap takes the velocity the finger had and carries it into the settle, so there is no seam between the two — no frame where the drag stops and an animation starts.

One rule that gets left out of every hand-rolled picker: drag fast, hold still for a moment, then lift. If the velocity is measured over a window that has gone stale, the wheel is thrown across the list by a speed your finger stopped having a quarter of a second ago. The finger stopped, so the wheel stops.

The version that quietly doesn’t work

The cheap build is a scroller with scroll-snap-type: y mandatory. Twelve lines, no maths, and in a screenshot it’s the same component. What it costs you is everything on either side of the gesture. The browser owns the deceleration, so glide isn’t yours to set; snapping happens after the momentum ends rather than being aimed at from the release; and there is no rubber-band at the ends worth the name, because the scroller is already at its limit and has nowhere to give.

Worse, it can’t be interrupted properly. Grab a snapping scroller mid-flight and you fight it; grab this one and it resumes from exactly the pixel that’s painted, because a single number in a ref is driving every row and that number is always the truth. Set give to 0.00 to feel the alternative — a hard wall reads as software that stopped listening, where resistance reads as a real thing with nothing more behind it.

A listbox in costume

The cylinder is a drawing. Underneath, each wheel is one tab stop with role="listbox", rows that are options, and aria-activedescendant moving with the detent — arrows step, page keys jump five, home and end go to the ends. Hold an arrow down and it stops springing and starts jumping, because a keystroke queued behind a 320ms spring is a keystroke you’ve broken; only the last one settles.

The focus ring goes around the lit band rather than the port. The port is 308px of scenery and the band is the part the arrow keys move, so that’s what gets the outline. With reduced motion the drag still tracks the finger one-to-one — that’s not an animation, it’s the control — and only the release changes: straight to the value, no throw, no settle. The barrel stays, since a shape isn’t motion.

Drag barrel to 0.00 and the cylinder flattens into a plain list that still throws, lands, and rubber-bands identically. That’s the honest test of which half is doing the work.