The bug
On my About page there's an Interstellar card. On desktop, you hover it and a little ring ship flies close to a black hole. Up close, tapping the black hole fast fires the ship's thrusters. On phones, tapping did... nothing. The card flew close, but the thrusters never fired.
What was actually happening
Here's the order of events for one tap:
pointerenter→ my hover code addedis-close.pointerleave→ my hover code removedis-close.clickon the black hole → "is the ship close?" No. Ignore the tap.clickbubbles to the card → the tap toggle setsis-closeagain.
So the ship looked close, but every thruster tap was checked in the split second when it wasn't. Two handlers, each correct on its own, cancelling each other out.
The fix
card.addEventListener('pointerenter', function (e) {
if (e.pointerType === 'touch') return; // taps use click
card.classList.add('is-close');
});
card.addEventListener('pointerleave', function (e) {
if (e.pointerType === 'touch') return;
card.classList.remove('is-close');
});Mouse and pen still get hover. Touch gets one clear path: tap to open, tap to close.
How to catch these before a client does
- Log the event order once on a real phone. You'll be surprised what a tap sends.
- Don't trust desktop emulation alone. Device mode fakes the screen size, but its clicks still arrive as mouse clicks.
- Test with synthetic events when you can't hold the device: dispatch
PointerEvents withpointerType: 'touch'in the order a real tap uses, then assert the state.
The general lesson
Hover is a mouse idea. On touch there's no "hovering", only a tap that briefly pretends. Any interaction that means something on hover needs a separate, deliberate answer for touch.