プルトゥリフレッシュ(Pull to Refresh)
このページでは、@use-gesture/react・Framer Motion・カスタム実装を使ったプルトゥリフレッシュの実装パターンをインタラクティブなデモとコードで確認できます。
プル検知アルゴリズム・インジケーターアニメーション・スナップバックの実装差を主要ライブラリで比較
💻 PC でご覧の方へ:デモはマウスドラッグでも操作できます。スクロール競合などタッチ特有の挙動はスマートフォンで確認できます。
@use-gesture/react
読み込み中...
tsx
'use client';
import { useState, useRef, useCallback } from 'react';
import { useDrag } from '@use-gesture/react';
import { useSpring, animated } from '@react-spring/web';
const THRESHOLD = 60; // px: リフレッシュ発動閾値
const MAX_PULL = 100; // px: 最大プル量
type Phase = 'idle' | 'pulling' | 'ready' | 'refreshing' | 'done';
export function UseGesturePullToRefreshDemo() {
const [phase, setPhase] = useState<Phase>('idle');
const phaseRef = useRef<Phase>('idle');
const containerRef = useRef<HTMLDivElement>(null);
const [spring, api] = useSpring(() => ({
y: 0, rotate: 0,
config: { tension: 280, friction: 26 },
}));
const doRefresh = useCallback(() => {
phaseRef.current = 'refreshing';
setPhase('refreshing');
api.start({ y: 48, rotate: 360 });
setTimeout(() => {
api.start({ y: 0, rotate: 0, config: { tension: 200, friction: 30 } });
setPhase('done');
phaseRef.current = 'done';
setTimeout(() => { setPhase('idle'); phaseRef.current = 'idle'; }, 1000);
}, 1200);
}, [api]);
const bind = useDrag(
({ active, movement: [, my], last, cancel }) => {
if (phaseRef.current === 'refreshing') { cancel?.(); return; }
const atTop = (containerRef.current?.scrollTop ?? 0) <= 0;
if (!atTop && my > 0) { cancel?.(); return; }
if (my <= 0) { api.start({ y: 0, rotate: 0 }); return; }
const pull = Math.min(my, MAX_PULL);
if (active) {
api.start({ y: pull, rotate: (pull / THRESHOLD) * 180, immediate: true });
const p = pull >= THRESHOLD ? 'ready' : 'pulling';
phaseRef.current = p; setPhase(p);
}
if (last) {
if (phaseRef.current === 'ready') doRefresh();
else { api.start({ y: 0, rotate: 0 }); phaseRef.current = 'idle'; setPhase('idle'); }
}
},
{ axis: 'y', pointer: { touch: true }, filterTaps: true }
);
return (
<div style={{ position: 'relative', height: '320px', overflow: 'hidden', border: '2px solid #e5e7eb', borderRadius: '1rem' }}>
{/* インジケーター(絶対配置) */}
<div style={{ position: 'absolute', top: 0, left: 0, right: 0, height: '48px', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 10, pointerEvents: 'none' }}>
<animated.div style={{ rotate: spring.rotate, opacity: spring.y.to(v => Math.min(v / 30, 1)), fontSize: '1.5rem' }}>
{phase === 'refreshing' ? '⟳' : '↓'}
</animated.div>
</div>
{/* スクロールリスト(プル量だけ下にシフト) */}
<animated.div ref={containerRef} {...bind()} style={{ y: spring.y, overflowY: 'auto', height: '100%', touchAction: 'pan-x' }}>
{/* リストアイテム */}
</animated.div>
</div>
);
}実装比較表
| 項目 | @use-gesture/react | Framer Motion | カスタム実装 |
|---|---|---|---|
| プル検知 | useDrag(Y軸)で EMA 平滑化済み速度を取得 | Pointer Events + useMotionValue でリアルタイム追跡 | pointerdown / pointermove / pointerup のみ |
| インジケーター | @react-spring/web の useSpring でスプリングアニメーション | useTransform でプル量に連動して回転・フェード | CSS transform + transition で translateY 制御 |
| スナップバック | スプリング物理(tension / friction)で自然な戻り | animate() で Spring スナップバック | CSS transition: transform 0.3s ease |
| 依存ライブラリ | @use-gesture/react + @react-spring/web | framer-motion のみ | なし(React + Pointer Events API のみ) |
| ローディング中アニメーション | rotate: 360 のスプリングループ | animate={{ rotate: [0, 360] }} + repeat: Infinity | なし(テキスト表示のみ) |
| マウス対応 | pointer: { touch: true } でタッチ優先、マウスも動作 | Pointer Events はマウス・タッチ共通 | Pointer Events はマウス・タッチ共通 |
⚠️ iOS Safari での注意点
- • iOS Safari にはネイティブのプルトゥリフレッシュがあるため、
overscroll-behavior-y: containをコンテナに設定して競合を防ぐこと - •
touchAction: "pan-x"を設定することで、縦スクロールとプル操作を Pointer Events で制御できる - • スクロールコンテナが
overflow: hiddenの親に包まれている場合、scrollTopの読み取りが正常に動作しないケースがある - •
setPointerCaptureを使うことで、要素外にポインターが移動してもpointermoveを継続して受け取れる
🤖 AIプロンプトテンプレート
Reactでプルトゥリフレッシュ(Pull to Refresh)を実装してください。以下の要件を満たしてください。 - @use-gesture/react、Framer Motion、またはPointer Events APIのいずれかを使用すること - スクロールコンテナの先頭(scrollTop === 0)でのみプル検知を有効にすること - プル量に連動してインジケーターが出現し、一定距離(60px)で「離してリフレッシュ」状態になること - 指を離した際にローディングアニメーションを表示し、完了後にリストを更新すること - スナップバックアニメーションを実装し、自然な操作感にすること - マウスドラッグでも動作するように実装すること(PC での開発・確認用)
⚠️ このプロンプトはあくまでたたき台です。AIの回答はモデルやバージョン、会話の文脈によって毎回異なります。意図通りに動かない場合は、条件を追記・修正してお使いください。