← Back to journal
May 27, 2026·4 min read·Building Trim

iPad on the ramp — why drag UIs need Pointer Events, not Mouse Events

A CFI texted me from the ramp: 'Trim won't let me drag a lesson on my iPad.' Cost me an hour. Root cause: iOS Safari doesn't dispatch mousemove during a finger drag. Here's the fix and the rule I'm saving forever.

By David Sawires
Share

A CFI in our beta sent me a single text message: "Trim won't let me drag a lesson on the iPad." He was sitting in his Cessna on the ramp, prepping for the next student. He had ten minutes between flights to rearrange his week.

The page loaded. The lesson cards rendered. He could tap them. He could open them. He just could not drag them across the calendar. On his desktop browser — same account, same data, same code — drag worked perfectly.

The bug

Trim's week calendar lets you grab a lesson block and drag it to a new time slot. The drag handler was, like a thousand other web apps, listening for onMouseDown / onMouseMove / onMouseUp. On a real mouse this works. On an iPad doing a finger drag, iOS Safari does not dispatch mousemove events at all. It dispatches touchstart, touchmove, touchend. So the drag handler heard the press, never heard the move, never heard the release. The card stayed where it started. The user concluded: the app is broken.

The fix

Don't listen for mouse events. Listen for Pointer Events:

onPointerDown={(ev) => {
  if (ev.pointerType === "mouse" && ev.button !== 0) return;
  ev.currentTarget.setPointerCapture(ev.pointerId);
  // ...begin drag
}}

Pointer Events were standardized to unify mouse + touch + pen + stylus into a single API. They fire for fingers. They fire for trackpads. They fire for an Apple Pencil. setPointerCapture is the magic — once you call it on pointer-down, every subsequent pointermove and pointerupfor that pointer ID is delivered to your element even if the finger drags outside it. That's exactly what dragging needs.

Two more requirements for finger drag to work reliably on iOS: set touch-action: none in CSS on the draggable element (otherwise Safari steals the gesture for native scroll), and listen for pointercancel(iOS fires this if a system gesture takes over, like a Notification Center pull). Without those two, you'll get a 90%-working drag that randomly gives up.

Any draggable, resizable, or hold-and-move UI uses Pointer Events. Mouse Events are for things that only happen to a cursor.

The rule I'm saving

This goes in the permanent memory file. Every future drag UI in any project I touch will start with Pointer Events. Mouse Events are reserved for things that only ever happen to a cursor — hover affordances, right-click menus on desktop. Anything that involves picking up an object and moving it is Pointer Events, no exceptions.

The bug was a one-line fix in the commit (741812b) but it was a one-hour debug because I started by assuming "iPad Safari is weird" and not by reading the actual spec. The spec was clear: mouse events are not dispatched during touch interactions unless you specifically opt in with touch-action: manipulation hacks, and even then the timing is unreliable.

The hidden lesson

A CFI sitting in a Cessna with ten minutes to prep is the most realistic stress-test environment a software product can have. Desktop dev tools never showed this bug. My local iPad simulator wouldn't have caught it either — the simulator runs WebKit but doesn't reliably emulate touch input timing the way a real device does. The only way this bug was going to surface was: ship it, watch a real CFI use it on a real iPad, listen.

The text message was three words long. The fix was one line. The rule is now permanent. That's the whole arc, and it only happens because beta CFIs are willing to send texts about what doesn't work.

Share