Skip to main content

Visual Feedback

Enhance user experience with clear visual indicators and responsive feedback.

Overview

Visual feedback provides users with immediate responses to their drag and drop actions. This example demonstrates:

  • Active state indicators
  • Drag state visualization
  • Drop zone highlighting
  • Success/error feedback

Key Features

  • Real-time Feedback: Immediate visual responses to user actions
  • State Indicators: Clear visual states for different interaction phases
  • Smooth Transitions: Fluid animations between states
  • Accessibility: High contrast and clear visual cues
  • Customizable: Flexible styling options

Basic Implementation

import React, { useState } from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
withTiming,
} from 'react-native-reanimated';
import { DropProvider, Draggable, Droppable } from 'react-native-reanimated-dnd';

export function VisualFeedbackExample() {
const [dragState, setDragState] = useState<'idle' | 'dragging' | 'success' | 'error'>('idle');
const [activeZone, setActiveZone] = useState<string | null>(null);

return (
<GestureHandlerRootView style={styles.container}>
<DropProvider>
<View style={styles.content}>
<Text style={styles.title}>Visual Feedback</Text>

{/* Status Indicator */}
<View style={styles.statusContainer}>
<View style={[styles.statusDot, styles[`${dragState}Dot`]]} />
<Text style={styles.statusText}>
{dragState === 'idle' && 'Ready to drag'}
{dragState === 'dragging' && 'Dragging...'}
{dragState === 'success' && 'Drop successful!'}
{dragState === 'error' && 'Drop failed'}
</Text>
</View>

{/* Draggable with Feedback */}
<FeedbackDraggable
data={{ id: 'feedback-item', label: 'Drag Me' }}
onDragStart={() => setDragState('dragging')}
onDragEnd={() => setDragState('idle')}
/>

{/* Drop Zones with Feedback */}
<View style={styles.dropZonesContainer}>
<FeedbackDropZone
id="success-zone"
title="✅ Success Zone"
isActive={activeZone === 'success-zone'}
onDragEnter={() => setActiveZone('success-zone')}
onDragLeave={() => setActiveZone(null)}
onDrop={() => {
setDragState('success');
setTimeout(() => setDragState('idle'), 2000);
}}
/>

<FeedbackDropZone
id="error-zone"
title="❌ Error Zone"
isActive={activeZone === 'error-zone'}
onDragEnter={() => setActiveZone('error-zone')}
onDragLeave={() => setActiveZone(null)}
onDrop={() => {
setDragState('error');
setTimeout(() => setDragState('idle'), 2000);
}}
/>
</View>
</View>
</DropProvider>
</GestureHandlerRootView>
);
}

function FeedbackDraggable({ data, onDragStart, onDragEnd }) {
const scale = useSharedValue(1);
const rotation = useSharedValue(0);
const opacity = useSharedValue(1);

const animatedStyle = useAnimatedStyle(() => ({
transform: [
{ scale: scale.value },
{ rotate: `${rotation.value}deg` }
],
opacity: opacity.value,
}));

const handleDragStart = () => {
scale.value = withSpring(1.1);
rotation.value = withSpring(5);
opacity.value = withTiming(0.9);
onDragStart?.();
};

const handleDragEnd = () => {
scale.value = withSpring(1);
rotation.value = withSpring(0);
opacity.value = withTiming(1);
onDragEnd?.();
};

return (
<Draggable
data={data}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
style={styles.draggableContainer}
>
<Animated.View style={[styles.draggable, animatedStyle]}>
<Text style={styles.draggableText}>{data.label}</Text>
<Text style={styles.dragHint}>Drag for feedback</Text>
</Animated.View>
</Draggable>
);
}

function FeedbackDropZone({ id, title, isActive, onDragEnter, onDragLeave, onDrop }) {
const scale = useSharedValue(1);
const borderWidth = useSharedValue(2);
const backgroundColor = useSharedValue(0);

const animatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }],
borderWidth: borderWidth.value,
backgroundColor: `rgba(88, 166, 255, ${backgroundColor.value})`,
}));

React.useEffect(() => {
if (isActive) {
scale.value = withSpring(1.02);
borderWidth.value = withTiming(4);
backgroundColor.value = withTiming(0.1);
} else {
scale.value = withSpring(1);
borderWidth.value = withTiming(2);
backgroundColor.value = withTiming(0);
}
}, [isActive]);

return (
<Droppable
droppableId={id}
onDrop={onDrop}
onDragEnter={onDragEnter}
onDragLeave={onDragLeave}
>
<Animated.View style={[styles.dropZone, animatedStyle]}>
<Text style={styles.zoneTitle}>{title}</Text>
<Text style={styles.zoneSubtitle}>
{isActive ? 'Release to drop' : 'Drag items here'}
</Text>
</Animated.View>
</Droppable>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#000000',
},
content: {
flex: 1,
padding: 20,
},
title: {
fontSize: 24,
fontWeight: 'bold',
color: '#FFFFFF',
textAlign: 'center',
marginBottom: 30,
},
statusContainer: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
marginBottom: 30,
padding: 16,
backgroundColor: '#1a1a1a',
borderRadius: 12,
},
statusDot: {
width: 12,
height: 12,
borderRadius: 6,
marginRight: 12,
},
idleDot: {
backgroundColor: '#8E8E93',
},
draggingDot: {
backgroundColor: '#58a6ff',
},
successDot: {
backgroundColor: '#32d74b',
},
errorDot: {
backgroundColor: '#ff453a',
},
statusText: {
color: '#FFFFFF',
fontSize: 16,
fontWeight: '500',
},
draggableContainer: {
alignSelf: 'center',
marginBottom: 40,
},
draggable: {
width: 120,
height: 120,
backgroundColor: '#a2d2ff',
borderRadius: 20,
justifyContent: 'center',
alignItems: 'center',
shadowColor: '#000',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.3,
shadowRadius: 6,
elevation: 8,
},
draggableText: {
color: '#000000',
fontWeight: 'bold',
fontSize: 16,
marginBottom: 4,
},
dragHint: {
color: '#333333',
fontSize: 12,
},
dropZonesContainer: {
flex: 1,
gap: 20,
},
dropZone: {
height: 120,
borderRadius: 16,
borderColor: '#58a6ff',
borderStyle: 'dashed',
justifyContent: 'center',
alignItems: 'center',
padding: 20,
},
zoneTitle: {
color: '#FFFFFF',
fontSize: 18,
fontWeight: 'bold',
marginBottom: 8,
},
zoneSubtitle: {
color: '#8E8E93',
fontSize: 14,
textAlign: 'center',
},
});

Feedback Types

Drag State Feedback

function DragStateFeedback({ isDragging }) {
return (
<View style={[
styles.dragIndicator,
isDragging && styles.activeDragIndicator
]}>
<Text style={styles.indicatorText}>
{isDragging ? 'Dragging...' : 'Ready'}
</Text>
</View>
);
}

Drop Zone Highlighting

function HighlightDropZone({ isActive, canDrop }) {
const borderColor = canDrop ? '#32d74b' : '#ff453a';

return (
<View style={[
styles.dropZone,
isActive && { borderColor, borderWidth: 3 }
]}>
{/* Zone content */}
</View>
);
}

Success/Error Animations

function FeedbackAnimation({ type }) {
const scale = useSharedValue(1);
const opacity = useSharedValue(1);

const animatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }],
opacity: opacity.value,
}));

React.useEffect(() => {
if (type === 'success') {
scale.value = withSequence(
withTiming(1.2, { duration: 200 }),
withSpring(1)
);
} else if (type === 'error') {
scale.value = withSequence(
withTiming(0.9, { duration: 100 }),
withTiming(1.1, { duration: 100 }),
withSpring(1)
);
}
}, [type]);

return (
<Animated.View style={[styles.feedback, animatedStyle]}>
<Text style={styles.feedbackText}>
{type === 'success' ? '✅' : '❌'}
</Text>
</Animated.View>
);
}

Advanced Feedback Patterns

Proximity Feedback

function ProximityFeedback({ distance, threshold = 50 }) {
const intensity = Math.max(0, 1 - distance / threshold);

return (
<View style={[
styles.proximityIndicator,
{ opacity: intensity, transform: [{ scale: 0.8 + intensity * 0.2 }] }
]}>
<Text style={styles.proximityText}>Drop Zone Near</Text>
</View>
);
}

Directional Indicators

function DirectionalIndicator({ direction }) {
const arrows = {
up: '↑',
down: '↓',
left: '←',
right: '→'
};

return (
<View style={styles.directionIndicator}>
<Text style={styles.directionArrow}>
{arrows[direction]}
</Text>
</View>
);
}

Progress Feedback

function ProgressFeedback({ progress }) {
return (
<View style={styles.progressContainer}>
<View style={[styles.progressBar, { width: `${progress}%` }]} />
<Text style={styles.progressText}>{Math.round(progress)}%</Text>
</View>
);
}

Accessibility Features

Screen Reader Support

<Draggable
data={itemData}
accessibilityLabel="Draggable item"
accessibilityHint="Double tap and hold to drag"
accessibilityRole="button"
>
{/* Content */}
</Draggable>

High Contrast Mode

function AccessibleDropZone({ isHighContrast }) {
return (
<View style={[
styles.dropZone,
isHighContrast && styles.highContrastZone
]}>
{/* Zone content */}
</View>
);
}

Haptic Feedback

import { HapticFeedback } from 'react-native';

function HapticDraggable({ data }) {
const handleDragStart = () => {
HapticFeedback.trigger('impactLight');
};

const handleDrop = () => {
HapticFeedback.trigger('notificationSuccess');
};

return (
<Draggable
data={data}
onDragStart={handleDragStart}
onDrop={handleDrop}
>
{/* Content */}
</Draggable>
);
}

Performance Considerations

Optimized Animations

// Use worklets for better performance
const animatedStyle = useAnimatedStyle(() => {
'worklet';
return {
transform: [{ scale: scale.value }],
opacity: opacity.value,
};
}, []);

Debounced Feedback

const debouncedFeedback = useMemo(
() => debounce((state) => setFeedbackState(state), 100),
[]
);

Common Use Cases

  • File Uploads: Progress indicators and success/error states
  • Form Builders: Field placement feedback
  • Games: Interactive element responses
  • Data Visualization: Chart manipulation feedback
  • E-commerce: Shopping cart interactions

Best Practices

  1. Immediate Response: Provide instant visual feedback
  2. Clear States: Use distinct visual states for different actions
  3. Smooth Transitions: Avoid jarring state changes
  4. Accessibility: Support screen readers and high contrast
  5. Performance: Optimize animations for 60fps

Next Steps