手機選單按了沒反應?1 段 JS 解決觸控 300ms 延遲與重複觸發

你在手機上打開自己做的 WordPress 網站,點選右上角的漢堡選單,卻發現選單沒有立刻展開,而是頓了一下才出現;快速點兩下甚至會讓選單閃爍、關閉後又馬上打開,整個互動體驗很不穩定。問題出在兩個經典的行動端陷阱:瀏覽器預設的 300ms 點擊延遲,以及重複觸發事件未受控管。只要一段精心設計的 JS,就能同時解決這兩個痛點,不需要依賴整套框架或過重的第三方套件。

為什麼瀏覽器要故意延遲?認識 click 事件的 300ms 空窗期

早期行動裝置為了支援「雙擊縮放(double-tap to zoom)」,會在 touchend 發生後等待約 300 毫秒,確認沒有第二次觸碰才觸發 click 事件。這段等待期就是選單反應遲鈍的主因。即使你在 <meta name="viewport"> 設定了 user-scalable=no,有些瀏覽器仍會保留部分延遲,而且強制禁用縮放對無障礙體驗並不友善。

一段 JS 同時解決延遲與重複觸發的核心寫法

最直接的解法是繞過 click,改監聽 touchstart 事件,並搭配狀態鎖避免連續觸發。以下是整合了即時反應與防重複邏輯的完整範例,適用於典型的手機選單開關情境:

(function() {
  const toggle = document.getElementById('menu-toggle');
  const menu   = document.getElementById('mobile-menu');
  let isAnimating = false;

  function openMenu() {
    menu.classList.add('is-active');
    toggle.setAttribute('aria-expanded', 'true');
  }

  function closeMenu() {
    menu.classList.remove('is-active');
    toggle.setAttribute('aria-expanded', 'false');
  }

  function handleToggle(e) {
    e.preventDefault(); // 阻止後續 click 與預設行為

    if (isAnimating) return; // 動畫進行中,忽略這次觸發
    isAnimating = true;

    const isOpen = menu.classList.contains('is-active');
    if (isOpen) {
      closeMenu();
    } else {
      openMenu();
    }

    // 監聽 CSS 過渡結束來解鎖,若無過渡則設定最短鎖定時間
    const onTransitionEnd = () => {
      isAnimating = false;
      menu.removeEventListener('transitionend', onTransitionEnd);
    };
    menu.addEventListener('transitionend', onTransitionEnd);
    // 備用計時器,防止 transitionend 沒觸發
    setTimeout(() => {
      if (isAnimating) {
        isAnimating = false;
        menu.removeEventListener('transitionend', onTransitionEnd);
      }
    }, 400);
  }

  toggle.addEventListener('touchstart', handleToggle, { passive: false });
  // 桌面端仍保留 click 作為備用,但利用旗標避免干擾
  toggle.addEventListener('click', function(e) {
    if (e.pointerType !== 'touch') {
      handleToggle(e);
    }
  });
})();

這段程式碼做了三件關鍵的事:以 touchstart 取代 click 來消除 300ms 延遲、透過 isAnimating 旗標避免動畫期間重複觸發,並在桌面端保留 click 作為輔助。使用 { passive: false } 是為了確保 preventDefault() 能生效,避免瀏覽器同時觸發後續的 click 與頁面滾動衝突。

💡 用一段程式碼達成雙重優勢

  • • 移除 300ms 延遲後,選單反應如同原生 App,大幅降低使用者挫折感。
  • • 狀態鎖與 transitionend 監聽確保動畫流暢,不會因快速連點出現畫面閃爍或選單卡在半開狀態。

延伸技巧:當選單需要更複雜的互動時

如果選單內還有子選單或手風琴元件,同樣可以套用上述模式。你只需要把 isAnimating 的管理邏輯抽成一個通用的輔助函式,或者搭配輕量的 debounce 工具。但要特別注意:在行動裝置上,touchstart + preventDefault 會阻止頁面捲動,因此只適合用在按鈕、選單開關等小範圍互動元素上,千萬別把整個選單容器設為這樣的事件監聽。

踩坑提醒與實務開發建議

一、務必為過渡動畫設定解鎖備援

如果 CSS 動畫因為任何原因未觸發 transitionend,選單會永久鎖住無法再次點擊。前面範例中的 setTimeout 備援機制就是為了預防這種情況,時間建議設為比 CSS transition 略長一點。

二、鍵盤與螢幕閱讀器不能放生

在桌面端或搭配輔助科技時,使用者可能只依賴鍵盤或語音操作。務必保留 click 監聽做為 fallback,並正確維護 aria-expanded 屬性,選單按鈕也應該是可以聚焦的 button 元素。

三、在 WordPress 中正確引入自訂 JS,避免污染全域

使用 wp_enqueue_script 並依賴 wp_footer 輸出腳本是正規做法。把上述邏輯放在一個立即執行函式(IIFE)裡,確保變數不會外洩到全域,降低與其他外掛衝突的風險。

四、測試時別只點一次,要模擬真實使用者的暴力操作

許多開發者在 Chrome DevTools 的裝置模式點一下看到選單正常開關就收工,但真正的手機使用者常常會快速連點、長按甚至邊滑動邊點到選單。記得在真機上瘋狂連點、快速開關,並確認沒有出現選單與頁面狀態不同步的情形。

文章分享