Handling click events inside touchmove containers on mobile Safari

I’m working on a mobile web app and I’ve got a scrollable div that uses touchstart, touchmove, and touchend listeners for custom scrolling behavior. The problem is that I need to call e.preventDefault() in the touchstart handler to stop the page from bouncing around during scrolling.

However, this is breaking all the clickable elements inside my scrollable container. Links and buttons don’t respond to taps anymore because I’m preventing the default touch behavior.

What’s the best way to handle this? I need both custom scrolling and working clickable elements. Has anyone dealt with this issue before on mobile Safari?

Hit this exact nightmare building a custom carousel. Here’s what actually worked - dual approach using touchmove distance tracking plus timing thresholds.

Don’t call preventDefault() in touchstart at all. Track touch coordinates and elapsed time instead. If the user moves 10px+ within 150ms, treat it as scrolling and block click events at touchend. Quick taps without movement go through normally.

Set a flag during touchmove when movement hits your threshold, then check it in click handlers. Safari’s native touch-to-click still works for stationary taps, but you intercept scroll gestures. Let the browser handle click conversion instead of fighting it with preventDefault.

safari touch events suck. use touch-action: manipulation css on your scrollable container instead of preventDefault. kills double-tap zoom but clicks still work. way easier than manually tracking coordinates and thresholds.

Been there. This exact issue drove me crazy for weeks when building touch interfaces.

The trick is selective preventDefault calls. Don’t blanket prevent all touch events. Instead, check if the touch target is clickable before calling preventDefault.

Here’s what works:

touchstartHandler(e) {
    const target = e.target;
    const isClickable = target.tagName === 'BUTTON' || 
                       target.tagName === 'A' || 
                       target.onclick || 
                       target.getAttribute('data-clickable');
    
    if (!isClickable) {
        e.preventDefault();
    }
    // your scroll logic here
}

You can also track touch distance and only prevent default if the user scrolls past a threshold.

But honestly, handling all these mobile touch edge cases manually is a pain. I automate this entire problem away now using Latenode. Set up workflows that detect user interactions and route them properly without fighting Safari’s quirks. Way cleaner than debugging touch event bubbling forever.

The Problem: You’re experiencing an issue where preventing default touch behavior (e.preventDefault()) in a touchstart event handler for a custom scrollable div prevents clickable elements (links, buttons) within that div from responding to taps on mobile Safari. You need a solution that allows both custom scrolling and functional clickable elements.

:thinking: Understanding the “Why” (The Root Cause):

The core issue is the indiscriminate use of e.preventDefault() in the touchstart handler. This method prevents all default browser behaviors associated with the touch event, including the click event that activates links and buttons. While necessary for preventing the default page scrolling behavior that interferes with your custom scrolling, it inadvertently blocks interactions with clickable elements within the scrollable area. The key is to be selective about when you call e.preventDefault(), only invoking it when genuinely needed to manage scrolling. A more sophisticated approach is required that distinguishes between scrolling gestures and tap gestures on clickable elements.

:gear: Step-by-Step Guide:

Step 1: Delayed preventDefault() Based on Gesture Detection: Instead of immediately calling e.preventDefault() in touchstart, defer the decision until you have more information about the user’s gesture. In the touchmove handler, track the distance the user has moved their finger. If the distance exceeds a threshold (e.g., 8 pixels) within a short timeframe (e.g., 150 milliseconds), it’s likely a scrolling gesture. Only then should you call e.preventDefault() in subsequent touchmove events.

Step 2: Implement a Scrolling Flag: Introduce a boolean flag (e.g., isScrolling) that’s set to true when the movement threshold in touchmove is exceeded.

Step 3: Conditional preventDefault() in touchmove: Inside your touchmove handler, check the isScrolling flag. If it’s true, call e.preventDefault(). If it’s false, let the default browser behavior proceed.

Step 4: Event Handling: Here’s how you might structure your event handlers:

let isScrolling = false;
let startX = 0;
let startY = 0;
let startTime = 0;

const touchstartHandler = (e) => {
  startX = e.touches[0].clientX;
  startY = e.touches[0].clientY;
  startTime = Date.now();
  isScrolling = false; // Reset the flag on each new touchstart
};

const touchmoveHandler = (e) => {
  const currentX = e.touches[0].clientX;
  const currentY = e.touches[0].clientY;
  const deltaX = currentX - startX;
  const deltaY = currentY - startY;
  const elapsedTime = Date.now() - startTime;

  const distanceThreshold = 8; // Adjust as needed
  const timeThreshold = 150;   // Adjust as needed

  if (Math.abs(deltaX) + Math.abs(deltaY) > distanceThreshold && elapsedTime < timeThreshold) {
    isScrolling = true;
  }

  if (isScrolling) {
    e.preventDefault();
  }
};

const touchendHandler = (e) => {
  isScrolling = false; // Reset for the next gesture
};

// Attach event listeners to your scrollable div
const scrollableDiv = document.getElementById('myScrollableDiv');
scrollableDiv.addEventListener('touchstart', touchstartHandler);
scrollableDiv.addEventListener('touchmove', touchmoveHandler);
scrollableDiv.addEventListener('touchend', touchendHandler);

Step 5: Testing and Refinement: Thoroughly test your implementation on various mobile devices and browsers, particularly Safari. Adjust the distanceThreshold and timeThreshold values to fine-tune the responsiveness and accuracy of your gesture detection.

:mag: Common Pitfalls & What to Check Next:

  • Threshold Values: The distanceThreshold and timeThreshold are crucial. Experiment to find values that work best for your app’s specific scrolling behavior and user interaction patterns. Too low, and accidental scrolling might be prevented; too high, and legitimate scrolling might not be intercepted.

  • Passive Event Listeners: Consider using passive event listeners ({ passive: true }) for touchstart and touchmove to improve scrolling performance. This allows the browser to optimize scrolling while still allowing you to selectively call e.preventDefault().

  • Event Bubbling: Ensure that your event handlers are correctly attached and that event bubbling isn’t interfering with the intended behavior.

:speech_balloon: Still running into issues? Share your (sanitized) config files, the exact command you ran, and any other relevant details. The community is here to help!

This topic was automatically closed 6 hours after the last reply. New replies are no longer allowed.