Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Prevent Double Tap Zoom in React for Rapidly Tapped Buttons

Prevent Double Tap Zoom in React for Rapidly Tapped Buttons

Touch delay is a thing of the past, but accidental zooming is here to ruin your day. Ever tapped a button quickly on iOS and experienced a zoom instead of two taps? You're in the right place.

Before

Before

After

After

Code

Just use this helper function to preventDefault on double taps, while still allowing for pinch to zoom and rapid scrolling.

// Ensure touches occur rapidly
const delay = 500
// Sequential touches must be in close vicinity
const minZoomTouchDelta = 10

// Track state of the last touch
let lastTapAt = 0
let lastClientX = 0
let lastClientY = 0

export default function preventDoubleTapZoom(event) {
  // Exit early if this involves more than one finger (e.g. pinch to zoom)
  if (event.touches.length > 1) {
    return
  }

  const tapAt = new Date().getTime()
  const timeDiff = tapAt - lastTapAt
  const { clientX, clientY } = event.touches[0]
  const xDiff = Math.abs(lastClientX - clientX)
  const yDiff = Math.abs(lastClientY - clientY)
  if (
    xDiff < minZoomTouchDelta &&
    yDiff < minZoomTouchDelta &&
    event.touches.length === 1 &&
    timeDiff < delay
  ) {
    event.preventDefault()
    // Trigger a fake click for the tap we just prevented
    event.target.click()
  }
  lastClientX = clientX
  lastClientY = clientY
  lastTapAt = tapAt
}

Example Use

const TapAsFastAsYouWantButton = (children, ...rest) =>
  <button {...props} onTouchStart={preventDoubleTapZoom}>
    {children}
  </button>

const Game = () =>
  <TapAsFastAsYouWantButton onClick={fireLasers}>
    Fire Lasers
  </TapAsFastAsYouWantButton>
@kachkaev

This comment has been minimized.

Copy link

kachkaev commented Oct 7, 2017

Hi @mutewinter! Great to see a potential fix to a problem that bothers me too! Do you think your solution can be used as a global preventor of all double clicks for all components? Similar to how react-fastclick works.

UPD: Own solution: https://stackoverflow.com/a/46624015/1818285

@jonathanmv

This comment has been minimized.

Copy link

jonathanmv commented Dec 19, 2017

My double tap to zoom was still happening after using this function and a warning was shown in the console

Unable to preventDefault inside passive event listener due to target being treated as passive. See https://www.chromestatus.com/features/5093566007214080

Based on this answer I set the touch-action: none; in the element's css and it worked for me. I haven't tested it thoroughly though.

@thaytharma

This comment has been minimized.

Copy link

thaytharma commented Oct 2, 2018

Hey, do you have anything similar working for Vue.Js 2?

@wolframdeus

This comment has been minimized.

Copy link

wolframdeus commented Apr 21, 2020

Worked for me with some remaks. It is not allowed to add event listener on React's onTouchStart due to it adds passive event listener. You have to use refs for target container and add event listener like rootRef.current.addEventListener('touchstart', onTouchStart, {passive: false}).

The other way you can make it easier is to add event listener to document.body.

Leaving React Hooks Typescript alternative:

import {useCallback, useRef} from 'react';

type OnTouchStart = (event: TouchEvent) => void;

interface Meta {
  lastTapAt: number;
  lastClientX: number;
  lastClientY: number;
}

/**
 * Returns handler which prevents from scrolling while double tapping
 * screen
 */
export function useDoubleTap(): OnTouchStart {
  // Ensure touches occur rapidly
  const delay = 1000;
  // Sequential touches must be in close vicinity
  const minZoomTouchDelta = 10;

  // Track state of the last touch
  const meta = useRef<Meta>({
    lastTapAt: 0,
    lastClientX: 0,
    lastClientY: 0,
  });

  return useCallback<OnTouchStart>(event => {
    // Exit early if this involves more than one finger (e.g. pinch to zoom)
    if (event.touches.length > 1) {
      return;
    }

    const tapAt = new Date().getTime();
    const timeDiff = tapAt - meta.current.lastTapAt;
    const {clientX, clientY} = event.touches[0];
    const xDiff = Math.abs(meta.current.lastClientX - clientX);
    const yDiff = Math.abs(meta.current.lastClientY - clientY);
    if (
      xDiff < minZoomTouchDelta &&
      yDiff < minZoomTouchDelta &&
      event.touches.length === 1 &&
      timeDiff < delay
    ) {
      event.preventDefault();

      // Trigger a fake click for the tap we just prevented
      if (event.target) {
        event.target.dispatchEvent(new Event('click', {
          bubbles: true,
          cancelable: true,
        }));
      }
    }
    meta.current.lastClientX = clientX;
    meta.current.lastClientY = clientY;
    meta.current.lastTapAt = tapAt;
  }, []);
}
@Coridyn

This comment has been minimized.

Copy link

Coridyn commented Apr 24, 2020

There is a pure CSS fix that may work also: touch-action: manipulation;

The MDN page on touch-action describes it:

"[manipulation] disable[s] additional non-standard gestures such as double-tap to zoom. Disabling double-tap to zoom removes the need for browsers to delay the generation of click events when the user taps the screen."

It's also well supported; available since iOS 9.3 and Android Chrome 36.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.