You're Writing Mouse and Touch Handlers. Pointer Events Does Both.
A draggable card that works with mousedown/mousemove on desktop and breaks the moment you test it on a phone. The fix isn't a second set of touch listeners — it's deleting the mouse ones and switching to Pointer Events, which was built to handle mouse, touch, and pen as one input.

The demo went great. Drag the card, it follows your cursor, drop it anywhere — smooth, on every laptop in the room. Then someone picks up their phone, drags a thumb across the card — and nothing happens. No movement, no feedback, like the card isn't even listening.
You didn't write a buggy drag handler. You wrote half of one.
This is correct, tested, and works on exactly one category of device.
The instinct is to bolt on touch support: copy the three listeners, swap mouse* for touch*, convert e.clientX/e.clientY to e.touches[0].clientX/e.touches[0].clientY, and call it done.
Now it works on a phone, in isolation. But you're maintaining the same drag math twice, in two event systems that don't agree on anything — touch events report an array of active touch points (because a screen can have more than one finger on it at once), mouse events report exactly one cursor. And both paths share a bug neither the original mouse-only version nor this touch-added version ever fixed: drag fast enough, or past the edge of the box, and the "end" event stops firing, leaving the card stuck mid-drag.
That's not a mobile-only bug. It's the same bug on both input types, because mousemove and touchmove share the same design flaw.
mousemove only fires on the element it's attached to while the cursor is actually over that element — it's hit-tested live, every event. Flick the mouse fast enough, or drag past the edge of the box, and the browser stops delivering mousemove (and eventually mouseup) there at all; those events go to whatever's now under the cursor instead, which has no idea your drag was in progress. The card is left believing it's still being dragged, because the one event that was supposed to tell it otherwise was never sent to it.
Touch has a related but distinct failure mode: if the OS decides your drag gesture is actually a scroll or a browser gesture (a mid-drag notification pull-down, a back-swipe), it can cancel the touch sequence outright, firing touchcancel instead of touchend — and if you only wired a touchend listener because "that's how the drag ends," it never gets a chance to clean up.
Two input systems, two different reasons, one shared symptom: dragging gets stuck true, because neither API gives the element a reliable way to say "tell me how this interaction ends, every time, no matter what."
Runs right in your browser — poke at it and watch the concept react live.
The Pointer Events API replaces mouse* and touch* with a single stream — pointerdown, pointermove, pointerup, pointercancel — that fires identically whether the input is a mouse, a finger, or a stylus. You write it once:
Three things changed, and each one closes a specific gap the old code had:
- One set of listeners, no branching on input type.
e.clientX/e.clientYwork the same for mouse, touch, and pen — no more readinge.touches[0]on one path ande.clientXdirectly on the other. box.setPointerCapture(e.pointerId)is the actual fix for the stuck-drag bug. Once an element captures a pointer, every event for that pointer — includingpointermoveandpointerup— keeps firing on that element, even when the pointer moves outside its boundaries or off the page entirely. The box can't lose track of the drag because the browser is now routing events to it directly, not based on where the pointer happens to be.pointercancelis the cleanup path the old code didn't have. It fires when the browser interrupts the interaction for its own reasons — a touch sequence hijacked into a system gesture, for instance. Listening for it alongsidepointerupmeansdraggingalways gets reset, instead of staying stuck ontrueforever.
One more line matters for touch devices specifically: add touch-action: none in CSS on the draggable element.
Without it, the browser's own scroll/zoom gesture handling can still compete with your drag for the same touch input before your JavaScript gets a say. touch-action: none tells the browser "this element handles its own gestures," which is exactly what you're asking Pointer Events to do.
e.pointerTypetells you"mouse","touch", or"pen"when you actually need to branch — for a stylus app that wants pressure (e.pressure) and tilt (e.tiltX/e.tiltY) only from a pen, for instance. You branch when the behavior should differ, not just to read the coordinates.- Multi-touch stays sane. Each contact point gets its own
pointerId, so a canvas handling two fingers at once tracks two independent pointer streams instead of indexing into a shiftingtouchesarray. - It's been safe to rely on for years. Pointer Events shipped in every major engine — Safari was the last holdout and added support in 2019 — so this isn't a progressive-enhancement decision anymore; it's the default way to handle pointing input.
Think it clicked? Take the 7-question quiz →
Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.
Pointer Events quietly fixes two separate bugs with one API. The "nothing happens on a phone" bug goes away because there's only one set of listeners to remember to wire up, not two. And the "drag freezes mid-gesture" bug — the one that was hiding in the mouse-only version from the start, then rode along into the touch-added version unchanged — goes away because setPointerCapture() finally gives the element what neither old API did: a guarantee that it keeps hearing from the pointer until the interaction is actually over.
Go find the component in your codebase with a mousedown handler and a near-identical touchstart handler sitting a few lines apart. How much of that duplication would pointerdown alone have deleted? Tell me what you find.
- You Don't Need a WebSocket for That Live Feed
- Your 'Save' Button Makes Copies. The File System Access API Doesn't.
- I Built Cross-Tab Logout. The storage Event Skipped One Tab.
🚀 Want more like this? Every guide, playground, and quiz lives on bestpractic.org — open it and sign up free so the next one finds you.
Thanks for reading! Let's stay connected:
- ⭐ GitHub — follow me and star the projects: github.com/parsajiravand
- 💬 Discord — join the frontend best-practices community: discord.gg/d9KRhuAwQ
- 📸 Instagram — frontend best practices, daily: @bestpractice___
Keep reading
One post a day, in your inbox
Each one with a runnable playground and a quiz. No pitch, no digest, unsubscribe in one click.
0 comments
Sign in to join the discussion, like comments, and save articles for later.