Skip to Content

FlutterGestureDetector

FlutterGestureDetector wraps Flutter’s native GestureDetector widget, enabling rich gesture interactions in your WebF applications. It provides native-level gesture recognition for tap, double tap, long press, pan (drag), pinch-to-scale, and rotation gestures.


Why Use FlutterGestureDetector?

While you can use standard web event handlers like onClick and onTouchMove, FlutterGestureDetector provides:

  • Native gesture recognition using Flutter’s gesture system
  • Multi-touch support for pinch and rotate gestures
  • Better performance with native gesture processing
  • Consistent behavior across platforms (iOS and Android)
  • Advanced gestures like double tap and long press with proper timing

Basic Usage

import { FlutterGestureDetector } from '@openwebf/react-core-ui'; function GestureExample() { const handleTap = (event: CustomEvent) => { console.log('Tapped!', event.detail); }; const handleDoubleTap = (event: CustomEvent) => { console.log('Double tapped!', event.detail); }; return ( <FlutterGestureDetector onTap={handleTap} onDoubletap={handleDoubleTap} > <div className="gesture-area"> Tap or double tap me! </div> </FlutterGestureDetector> ); }

Tap Gestures

Handle single tap, double tap, and long press events:

function TapGestures() { const [tapCount, setTapCount] = useState(0); const [doubleTapCount, setDoubleTapCount] = useState(0); const [longPressCount, setLongPressCount] = useState(0); const [isLongPressing, setIsLongPressing] = useState(false); return ( <FlutterGestureDetector onTap={(e) => { setTapCount(prev => prev + 1); console.log('Tap at:', e.detail); }} onDoubletap={(e) => { setDoubleTapCount(prev => prev + 1); console.log('Double tap at:', e.detail); }} onLongpress={(e) => { setLongPressCount(prev => prev + 1); setIsLongPressing(true); console.log('Long press started:', e.detail); }} onLongpressend={(e) => { setIsLongPressing(false); console.log('Long press ended:', e.detail); }} > <div className="tap-area"> <p>Taps: {tapCount}</p> <p>Double Taps: {doubleTapCount}</p> <p>Long Presses: {longPressCount}</p> <p>Status: {isLongPressing ? 'Long Pressing...' : 'Ready'}</p> </div> </FlutterGestureDetector> ); }

Pan (Drag) Gestures

Track dragging movements with start, update, and end events:

function PanGestures() { const [panState, setPanState] = useState({ isPanning: false, position: { x: 0, y: 0 }, delta: { x: 0, y: 0 }, velocity: { x: 0, y: 0 } }); return ( <FlutterGestureDetector onPanstart={(e) => { const { x, y } = e.detail; setPanState(prev => ({ ...prev, isPanning: true, position: { x, y }, delta: { x: 0, y: 0 } })); }} onPanupdate={(e) => { const { x, y, deltaX, deltaY } = e.detail; setPanState(prev => ({ ...prev, position: { x, y }, delta: { x: deltaX, y: deltaY } })); }} onPanend={(e) => { const { velocityX, velocityY } = e.detail; setPanState(prev => ({ ...prev, isPanning: false, velocity: { x: velocityX, y: velocityY } })); }} > <div className="pan-area"> <p>Status: {panState.isPanning ? 'Panning' : 'Idle'}</p> <p>Position: ({panState.position.x.toFixed(0)}, {panState.position.y.toFixed(0)})</p> <p>Delta: ({panState.delta.x.toFixed(0)}, {panState.delta.y.toFixed(0)})</p> <p>Velocity: ({panState.velocity.x.toFixed(2)}, {panState.velocity.y.toFixed(2)})</p> </div> </FlutterGestureDetector> ); }

Pan event details:

  • x, y - Current position coordinates
  • deltaX, deltaY - Movement delta from previous update
  • velocityX, velocityY - Gesture velocity on end (for inertia effects)

Scale & Rotate Gestures

Handle pinch-to-zoom and rotation with multi-touch:

function ScaleRotateGestures() { const [scaleState, setScaleState] = useState({ isScaling: false, scale: 1.0, rotation: 0, focalPoint: { x: 0, y: 0 } }); return ( <FlutterGestureDetector onScalestart={(e) => { setScaleState(prev => ({ ...prev, isScaling: true })); }} onScaleupdate={(e) => { const { scale, rotation, focalPointX, focalPointY } = e.detail; setScaleState(prev => ({ ...prev, scale, rotation, focalPoint: { x: focalPointX, y: focalPointY } })); }} onScaleend={(e) => { setScaleState(prev => ({ ...prev, isScaling: false })); }} > <div className="scale-area" style={{ transform: `scale(${scaleState.scale}) rotate(${scaleState.rotation}rad)`, transformOrigin: `${scaleState.focalPoint.x}px ${scaleState.focalPoint.y}px` }} > <p>Status: {scaleState.isScaling ? 'Scaling' : 'Idle'}</p> <p>Scale: {scaleState.scale.toFixed(2)}x</p> <p>Rotation: {(scaleState.rotation * 180 / Math.PI).toFixed(0)}°</p> <p>Focal Point: ({scaleState.focalPoint.x.toFixed(0)}, {scaleState.focalPoint.y.toFixed(0)})</p> </div> </FlutterGestureDetector> ); }

Scale event details:

  • scale - Scale factor (1.0 = original size)
  • rotation - Rotation in radians
  • focalPointX, focalPointY - Center point of the pinch gesture

Complete Gesture Handler

Combine multiple gestures in a single component:

function CompleteGestureExample() { const [gestureInfo, setGestureInfo] = useState({ currentGesture: 'none', details: '' }); return ( <FlutterGestureDetector onTap={(e) => { setGestureInfo({ currentGesture: 'tap', details: `Tapped at (${e.detail.x}, ${e.detail.y})` }); }} onDoubletap={(e) => { setGestureInfo({ currentGesture: 'double-tap', details: 'Double tapped!' }); }} onLongpress={(e) => { setGestureInfo({ currentGesture: 'long-press', details: 'Long press started' }); }} onLongpressend={(e) => { setGestureInfo({ currentGesture: 'long-press-end', details: 'Long press ended' }); }} onPanstart={(e) => { setGestureInfo({ currentGesture: 'pan', details: `Pan started at (${e.detail.x}, ${e.detail.y})` }); }} onPanupdate={(e) => { setGestureInfo({ currentGesture: 'pan', details: `Delta: (${e.detail.deltaX.toFixed(0)}, ${e.detail.deltaY.toFixed(0)})` }); }} onPanend={(e) => { setGestureInfo({ currentGesture: 'pan-end', details: `Velocity: (${e.detail.velocityX.toFixed(2)}, ${e.detail.velocityY.toFixed(2)})` }); }} onScalestart={(e) => { setGestureInfo({ currentGesture: 'scale', details: 'Scale started' }); }} onScaleupdate={(e) => { setGestureInfo({ currentGesture: 'scale', details: `Scale: ${e.detail.scale.toFixed(2)}x, Rotation: ${(e.detail.rotation * 180 / Math.PI).toFixed(0)}°` }); }} onScaleend={(e) => { setGestureInfo({ currentGesture: 'scale-end', details: 'Scale ended' }); }} > <div className="interactive-area"> <h3>Try all gestures:</h3> <ul> <li>Tap</li> <li>Double Tap</li> <li>Long Press</li> <li>Pan (Drag)</li> <li>Pinch (two fingers)</li> <li>Rotate (two fingers)</li> </ul> <div className="gesture-status"> <strong>Current:</strong> {gestureInfo.currentGesture}<br /> <strong>Details:</strong> {gestureInfo.details} </div> </div> </FlutterGestureDetector> ); }

Props

PropTypeDescription
onTap(event: CustomEvent) => voidSingle tap event
onDoubletap(event: CustomEvent) => voidDouble tap event
onLongpress(event: CustomEvent) => voidLong press started
onLongpressend(event: CustomEvent) => voidLong press ended
onPanstart(event: CustomEvent) => voidPan gesture started
onPanupdate(event: CustomEvent) => voidPan gesture updated
onPanend(event: CustomEvent) => voidPan gesture ended
onScalestart(event: CustomEvent) => voidScale/rotate gesture started
onScaleupdate(event: CustomEvent) => voidScale/rotate gesture updated
onScaleend(event: CustomEvent) => voidScale/rotate gesture ended
classNamestringCSS class name
styleCSSPropertiesInline styles
childrenReactNodeContent to wrap with gesture detection

Event Details

Tap Events (onTap, onDoubletap)

{ x: number; // X coordinate y: number; // Y coordinate }

Long Press Events (onLongpress, onLongpressend)

{ x: number; // X coordinate y: number; // Y coordinate }

Pan Events

onPanstart:

{ x: number; // Starting X coordinate y: number; // Starting Y coordinate }

onPanupdate:

{ x: number; // Current X coordinate y: number; // Current Y coordinate deltaX: number; // Change in X since last update deltaY: number; // Change in Y since last update }

onPanend:

{ velocityX: number; // Horizontal velocity velocityY: number; // Vertical velocity }

Scale Events

onScaleupdate:

{ scale: number; // Scale factor (1.0 = original) rotation: number; // Rotation in radians focalPointX: number; // Center point X focalPointY: number; // Center point Y }

Best Practices

  1. Handle all phases - For pan and scale gestures, handle start, update, and end events
  2. Use appropriate gestures - Choose gestures that match user expectations
  3. Provide visual feedback - Show users when gestures are active
  4. Test on devices - Multi-touch gestures work best on actual devices
  5. Avoid gesture conflicts - Don’t use conflicting gestures on nested elements

Styling Example

import { FlutterGestureDetector } from '@openwebf/react-core-ui'; function StyledGestureArea() { const [isActive, setIsActive] = useState(false); return ( <FlutterGestureDetector className={`gesture-detector ${isActive ? 'active' : ''}`} onPanstart={() => setIsActive(true)} onPanend={() => setIsActive(false)} onScalestart={() => setIsActive(true)} onScaleend={() => setIsActive(false)} > <div className="gesture-content"> Drag or pinch to interact </div> </FlutterGestureDetector> ); }
.gesture-detector { padding: 32px; border: 2px dashed #d1d5db; border-radius: 12px; background: linear-gradient(135deg, #f3f4f6 0%, #e5e7eb 100%); transition: all 0.2s; cursor: pointer; } .gesture-detector.active { border-color: #3b82f6; background: linear-gradient(135deg, #dbeafe 0%, #bfdbfe 100%); box-shadow: 0 4px 12px rgba(59, 130, 246, 0.2); } .gesture-content { text-align: center; font-size: 16px; color: #6b7280; user-select: none; } .gesture-detector.active .gesture-content { color: #1e40af; font-weight: 600; }

Performance Tips

  • Use React.memo() for components inside gesture detectors
  • Throttle rapid updates in onPanupdate and onScaleupdate if needed
  • Avoid heavy computations in gesture event handlers
  • Use CSS transforms for visual feedback (hardware accelerated)

Gesture Conflicts

Be aware of gesture priorities:

  1. Tap vs Double Tap - Double tap has a delay waiting to see if it’s a double tap
  2. Pan vs Scale - Scale requires two fingers, pan works with one
  3. Long Press vs Pan - Long press must be stationary; movement triggers pan

To handle conflicts, use only the gestures you need:

// Good: Use only what you need <FlutterGestureDetector onTap={handleTap}> {/* ... */} </FlutterGestureDetector> // Avoid: Using conflicting gestures unnecessarily <FlutterGestureDetector onTap={handleTap} onDoubletap={handleDoubleTap} // May cause tap delays onLongpress={handleLongPress} // May conflict with pan onPanstart={handlePan} > {/* ... */} </FlutterGestureDetector>

Next Steps