ロングプレス(タイムアウト・移動キャンセル判定)
タイムアウト時間・移動キャンセル閾値の設定差と、移動距離計算の精度を主要ライブラリで比較
💻 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/react | Framer Motion | Pointer 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 で精度が高い
useDrag の movement は 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: noneとuser-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の回答はモデルやバージョン、会話の文脈によって毎回異なります。意図通りに動かない場合は、条件を追記・修正してお使いください。