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
- Immediate Response: Provide instant visual feedback
- Clear States: Use distinct visual states for different actions
- Smooth Transitions: Avoid jarring state changes
- Accessibility: Support screen readers and high contrast
- Performance: Optimize animations for 60fps
Next Steps
- Explore Custom Animations for advanced effects
- Learn about Collision Detection for precise feedback
- Check out Drop Zones for zone-specific feedback