ロングプレス(タイムアウト・移動キャンセル判定)

タイムアウト時間・移動キャンセル閾値の設定差と、移動距離計算の精度を主要ライブラリで比較

💻 PC でご覧の方へ:3 つともマウスに対応。ボタン上で長押し(クリックホールド)するとプログレスが進み、マウスを動かすとキャンセル判定が発火します。タイムアウト・閾値を変更して挙動の違いを確認できます。
@use-gesture/react

読み込み中...

tsx
'use client';
import { useState } from 'react';
import { useLongPress } from '@use-gesture/react';

export function UseGestureLongPressDemo() {
  const [state, setState] = useState<'idle' | 'pressing' | 'triggered'>('idle');

  const bind = useLongPress(
    () => {
      setState('triggered');
      setTimeout(() => setState('idle'), 1000);
    },
    {
      threshold: 500,
      filterTaps: true,
      onStart: () => setState('pressing'),
      onCancel: () => setState('idle'),
    }
  );

  return (
    <div className="flex flex-col items-center gap-4 p-8">
      <button
        {...bind()}
        className={`w-32 h-32 rounded-2xl text-white font-bold text-sm select-none transition-colors ${
          state === 'triggered' ? 'bg-green-500' : state === 'pressing' ? 'bg-violet-400' : 'bg-violet-600'
        }`}
        style={{ touchAction: 'none' }}
      >
        {state === 'triggered' ? '✓ 発火!' : state === 'pressing' ? '長押し中...' : '長押しする'}
      </button>
      <p className="text-sm text-gray-500">500ms 長押しで発火(移動でキャンセル)</p>
    </div>
  );
}

タイムアウト・移動キャンセル比較表

項目@use-gesture/reactFramer MotionPointer Events API
タイムアウト管理手動 setTimeout(自由設定)手動 setTimeout(自由設定)手動 setTimeout(自由設定)
移動キャンセル閾値自由設定(px 指定)Framer 内部管理(非公開・変更不可)自由設定(px 指定)
移動距離の計算EMA フィルタ済み movement(ノイズ抑制)Framer 内部(取得不可・onTapCancel のみ)生 Euclidean √(Δx²+Δy²)(ノイズあり)
移動距離のリアルタイム取得可能(movement[0/1] から随時参照)不可(onTapCancel 発火のみ)可能(onPointerMove で随時計算)
ポインターキャプチャ自動(bind が内部で処理)自動(motion コンポーネントが処理)手動(setPointerCapture が必須)
システムキャンセル検出active=false で検出(pointercancel 含む)onTapCancel で検出onPointerCancel イベントを別途処理
実装コスト低〜中(移動検出は use-gesture に委任)低(移動判定委任・閾値制御不可の制約あり)中〜高(すべて手実装・LPF が必要な場合も)

移動キャンセル判定の実装差

@use-gesture — EMA 済み movement で精度が高い

useDragmovement は EMA(指数移動平均)でフィルタされているため、指の微小ブレ(±1〜2px のノイズ)が累積されにくい。閾値 5px 程度に設定しても誤キャンセルが発生しにくく、コンテキストメニューや振動フィードバックを実装する本番ユースケースに適している。

Framer Motion — 移動閾値はブラックボックス

onTapCancel が発火するまで移動距離を知る方法がない。閾値はデバイスや Framer のバージョンによって変わる可能性がある。「どの程度動いたらキャンセルする」という業務要件がある場合は Framer だけでは対応できず、onPointerMove を追加実装する必要がある。

Pointer Events API — 生 Euclidean で誤キャンセルリスク

√(Δx²+Δy²) の生値はタッチノイズをそのまま反映する。静止中でも指の圧力変化で ±1〜3px の揺らぎが生じるため、閾値 5px に設定すると意図しないキャンセルが頻発する。実用上は LPF(ローパスフィルタ)の実装か、閾値を 10〜15px 以上に設定することが推奨される。

⚠️ iOS Safari での注意点

  • • iOS Safari はロングプレスで「コピー・選択メニュー」を表示しようとするため、-webkit-touch-callout: noneuser-select: none の指定が必須
  • touch-action: none を忘れるとスクロールが優先され、ロングプレス開始直後に pointercancel が発火する
  • • Vanilla / use-gesture は setPointerCapture を呼ぶことで、指がボタン外に出ても pointerup を確実に受け取れる
  • • iOS 14 以前では pointer-events: none の要素に対して pointercancel が期待通りに発火しないケースがある
  • • 振動フィードバック(navigator.vibrate())は iOS Safari 未対応。Android Chrome では長押し成功時の触覚フィードバックとして活用できる

🤖 AIプロンプトテンプレート

Reactでロングプレス(長押し)を実装してください。以下の要件を満たしてください。

- @use-gesture/react、Framer Motion、またはPointer Events APIのいずれかを使用すること
- 長押し時間(デフォルト500ms)をミリ秒単位で設定できること
- 長押し中のプログレスバーまたはリングアニメーションで進捗を視覚化すること
- 指・カーソルが一定距離(例:5〜15px)以上移動した場合、長押しをキャンセルすること
- 長押し成功時にコンテキストメニューや確認ダイアログを表示すること
- iOS SafariでWebkitのコピーメニューが出ないようuser-select: noneを設定すること

⚠️ このプロンプトはあくまでたたき台です。AIの回答はモデルやバージョン、会話の文脈によって毎回異なります。意図通りに動かない場合は、条件を追記・修正してお使いください。