你在手機上打開自己做的 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 的裝置模式點一下看到選單正常開關就收工,但真正的手機使用者常常會快速連點、長按甚至邊滑動邊點到選單。記得在真機上瘋狂連點、快速開關,並確認沒有出現選單與頁面狀態不同步的情形。


