Skip to main content

Droppable Component

A component that creates drop zones for receiving draggable items with visual feedback and flexible positioning options.

Overview​

The Droppable component provides visual feedback when draggable items hover over it and handles the drop logic when items are released. It integrates seamlessly with the drag-and-drop context to provide collision detection and proper positioning of dropped items.

Import​

import { Droppable } from 'react-native-reanimated-dnd';

Props​

Core Props​

onDrop​

  • Type: (data: TData) => void
  • Required: Yes
  • Description: Callback function fired when an item is successfully dropped on this droppable. This is where you handle the drop logic for your application.
const handleDrop = (data) => {
console.log('Item dropped:', data.name);
moveItemToColumn(data.id, 'completed');
showNotification(`${data.name} completed!`);
};

<Droppable onDrop={handleDrop}>
<Text>Drop zone</Text>
</Droppable>

children​

  • Type: React.ReactNode
  • Required: Yes
  • Description: The content to render inside the droppable.

style​

  • Type: StyleProp<ViewStyle>
  • Required: No
  • Description: Style to apply to the droppable container.
<Droppable 
onDrop={handleDrop}
style={[styles.dropZone, { backgroundColor: '#f0f0f0' }]}
>
<Text>Styled drop zone</Text>
</Droppable>

Interaction Props​

dropDisabled​

  • Type: boolean
  • Default: false
  • Description: Whether this droppable is disabled. When true, items cannot be dropped here. Useful for conditionally enabling/disabling drop functionality.
const isDisabled = user.role !== 'admin';

<Droppable
onDrop={handleDrop}
dropDisabled={isDisabled}
>
<Text>Admin only drop zone</Text>
</Droppable>

droppableId​

  • Type: string
  • Required: No
  • Description: Unique identifier for this droppable. If not provided, one will be generated automatically. Used for tracking which droppable items are dropped on.
<Droppable 
droppableId="todo-column"
onDrop={handleDrop}
>
<Text>Todo Column</Text>
</Droppable>

Callback Props​

onActiveChange​

  • Type: (isActive: boolean) => void
  • Required: No
  • Description: Callback fired when the active state of this droppable changes. Active state indicates whether a draggable item is currently hovering over this droppable.
const handleActiveChange = (isActive) => {
if (isActive) {
playHoverSound();
setHighlighted(true);
} else {
setHighlighted(false);
}
};

<Droppable
onDrop={handleDrop}
onActiveChange={handleActiveChange}
>
<Text>Interactive drop zone</Text>
</Droppable>

Positioning Props​

dropAlignment​

  • Type: DropAlignment
  • Default: "center"
  • Description: How dropped items should be aligned within this droppable area.

Available alignments:

  • center: Center the item within the droppable (default)
  • top-left: Align to top-left corner
  • top-center: Align to top edge, centered horizontally
  • top-right: Align to top-right corner
  • center-left: Align to left edge, centered vertically
  • center-right: Align to right edge, centered vertically
  • bottom-left: Align to bottom-left corner
  • bottom-center: Align to bottom edge, centered horizontally
  • bottom-right: Align to bottom-right corner
// Items dropped here will snap to the top-left corner
<Droppable
onDrop={handleDrop}
dropAlignment="top-left"
>
<Text>Top-left aligned drops</Text>
</Droppable>

dropOffset​

  • Type: DropOffset
  • Required: No
  • Description: Additional pixel offset to apply after alignment. Useful for fine-tuning the exact position where items are dropped.
// Drop items 10px to the right and 5px down from the center
<Droppable
onDrop={handleDrop}
dropAlignment="center"
dropOffset={{ x: 10, y: 5 }}
>
<Text>Offset drop zone</Text>
</Droppable>

Visual Feedback Props​

activeStyle​

  • Type: StyleProp<ViewStyle>
  • Required: No
  • Description: Style to apply when a draggable item is hovering over this droppable. This provides visual feedback to users about valid drop targets.
const activeStyle = {
backgroundColor: 'rgba(0, 255, 0, 0.2)',
borderColor: '#00ff00',
borderWidth: 2,
transform: [{ scale: 1.05 }]
};

<Droppable
onDrop={handleDrop}
activeStyle={activeStyle}
>
<Text>Visual feedback drop zone</Text>
</Droppable>

Capacity Props​

capacity​

  • Type: number
  • Default: 1
  • Description: Maximum number of items that can be dropped on this droppable. When capacity is reached, additional items cannot be dropped here.
// Allow up to 5 items in this drop zone
<Droppable
onDrop={handleDrop}
capacity={5}
>
<Text>Limited capacity (5 items)</Text>
</Droppable>

// Unlimited capacity
<Droppable
onDrop={handleDrop}
capacity={Infinity}
>
<Text>Unlimited capacity</Text>
</Droppable>

Usage Examples​

Basic Drop Zone​

import { Droppable } from 'react-native-reanimated-dnd';

function BasicDropZone() {
const handleDrop = (data) => {
console.log('Item dropped:', data);
addItemToList(data);
};

return (
<Droppable onDrop={handleDrop}>
<View style={styles.dropZone}>
<Text>Drop items here</Text>
</View>
</Droppable>
);
}

Drop Zone with Visual Feedback​

function VisualDropZone() {
const [isHovered, setIsHovered] = useState(false);

const activeStyle = {
backgroundColor: 'rgba(34, 197, 94, 0.2)',
borderColor: '#22c55e',
borderWidth: 2,
borderStyle: 'dashed',
transform: [{ scale: 1.02 }]
};

return (
<Droppable
onDrop={(data) => {
console.log('Dropped:', data.name);
processDroppedItem(data);
}}
onActiveChange={setIsHovered}
activeStyle={activeStyle}
style={styles.dropZone}
>
<View style={[
styles.dropContent,
isHovered && styles.hoveredContent
]}>
<Icon
name={isHovered ? "check-circle" : "plus-circle"}
size={32}
color={isHovered ? '#22c55e' : '#6b7280'}
/>
<Text style={[styles.dropText, isHovered && styles.activeText]}>
{isHovered ? 'Release to drop' : 'Drag items here'}
</Text>
</View>
</Droppable>
);
}

Kanban Column​

function TaskColumn({ status, tasks, onTaskDrop }) {
const maxTasks = 10;
const isFull = tasks.length >= maxTasks;

return (
<Droppable
droppableId={`${status}-column`}
onDrop={(task) => {
if (!isFull) {
onTaskDrop(task, status);
updateTaskStatus(task.id, status);
}
}}
dropAlignment="top-center"
dropOffset={{ x: 0, y: 10 }}
capacity={maxTasks}
dropDisabled={isFull}
activeStyle={{
backgroundColor: 'rgba(59, 130, 246, 0.1)',
borderColor: '#3b82f6',
borderWidth: 2,
borderStyle: 'dashed'
}}
style={[styles.column, isFull && styles.fullColumn]}
>
<Text style={styles.columnTitle}>
{status.toUpperCase()} ({tasks.length}/{maxTasks})
</Text>

{tasks.map(task => (
<TaskCard key={task.id} task={task} />
))}

{tasks.length === 0 && (
<Text style={styles.emptyText}>
Drop tasks here
</Text>
)}

{isFull && (
<Text style={styles.fullText}>
Column is full
</Text>
)}
</Droppable>
);
}

File Upload Zone​

function FileUploadZone() {
const [uploadedFiles, setUploadedFiles] = useState([]);
const [isUploading, setIsUploading] = useState(false);

const handleFileDrop = async (fileData) => {
// Validate file
if (fileData.size > 10000000) { // 10MB limit
showError('File size must be under 10MB');
return;
}

setIsUploading(true);
try {
await uploadFile(fileData);
setUploadedFiles(prev => [...prev, fileData]);
showSuccess(`${fileData.name} uploaded successfully`);
} catch (error) {
showError('Upload failed');
} finally {
setIsUploading(false);
}
};

return (
<Droppable
onDrop={handleFileDrop}
dropDisabled={isUploading}
capacity={20}
activeStyle={{
backgroundColor: 'rgba(59, 130, 246, 0.1)',
borderColor: '#3b82f6',
borderWidth: 2,
borderStyle: 'dashed'
}}
onActiveChange={(active) => {
if (active && isUploading) {
showTooltip('Upload in progress...');
}
}}
style={[styles.uploadZone, isUploading && styles.uploading]}
>
<View style={styles.uploadContent}>
<Icon
name="cloud-upload"
size={48}
color={isUploading ? "#6b7280" : "#3b82f6"}
/>
<Text style={styles.uploadText}>
{isUploading ? 'Uploading...' : 'Drop files here'}
</Text>
<Text style={styles.fileCount}>
{uploadedFiles.length}/20 files uploaded
</Text>

{uploadedFiles.slice(-3).map(file => (
<View key={file.id} style={styles.recentFile}>
<Text>{file.name}</Text>
</View>
))}
</View>
</Droppable>
);
}

Conditional Drop Zone​

function ConditionalDropZone({ allowedTypes, userRole }) {
const [validDrop, setValidDrop] = useState(true);

const handleDrop = (data) => {
// Validate item type
if (!allowedTypes.includes(data.type)) {
showError(`${data.type} items not allowed here`);
return;
}

// Validate user permissions
if (userRole !== 'admin' && data.restricted) {
showError('Insufficient permissions');
return;
}

handleValidDrop(data);
};

const handleActiveChange = (active) => {
if (active) {
// Check if the hovering item is valid
const hoveringItem = getCurrentHoveringItem();
if (hoveringItem) {
const isValidType = allowedTypes.includes(hoveringItem.type);
const hasPermission = userRole === 'admin' || !hoveringItem.restricted;
setValidDrop(isValidType && hasPermission);
}
}
};

return (
<Droppable
onDrop={handleDrop}
onActiveChange={handleActiveChange}
activeStyle={{
backgroundColor: validDrop
? 'rgba(34, 197, 94, 0.2)'
: 'rgba(239, 68, 68, 0.2)',
borderColor: validDrop ? '#22c55e' : '#ef4444',
borderWidth: 2
}}
style={styles.conditionalZone}
>
<View style={styles.zoneContent}>
<Text style={styles.title}>Restricted Drop Zone</Text>
<Text style={styles.subtitle}>
Accepts: {allowedTypes.join(', ')}
</Text>
<Text style={styles.permission}>
Role: {userRole}
</Text>

{!validDrop && (
<Text style={styles.errorText}>
Invalid item or insufficient permissions
</Text>
)}
</View>
</Droppable>
);
}

Shopping Cart Drop Zone​

function ShoppingCartDropZone() {
const [cartItems, setCartItems] = useState([]);
const [total, setTotal] = useState(0);

const handleProductDrop = (product) => {
// Check if item already in cart
const existingItem = cartItems.find(item => item.id === product.id);

if (existingItem) {
// Increase quantity
setCartItems(prev => prev.map(item =>
item.id === product.id
? { ...item, quantity: item.quantity + 1 }
: item
));
} else {
// Add new item
setCartItems(prev => [...prev, { ...product, quantity: 1 }]);
}

// Update total
setTotal(prev => prev + product.price);

// Show feedback
showToast(`${product.name} added to cart`);
hapticFeedback();
};

return (
<Droppable
onDrop={handleProductDrop}
capacity={50}
activeStyle={{
backgroundColor: 'rgba(34, 197, 94, 0.1)',
borderColor: '#22c55e',
borderWidth: 2,
transform: [{ scale: 1.02 }]
}}
style={styles.cartZone}
>
<View style={styles.cartHeader}>
<Icon name="shopping-cart" size={24} />
<Text style={styles.cartTitle}>Shopping Cart</Text>
</View>

<Text style={styles.cartTotal}>
Total: ${total.toFixed(2)}
</Text>

<Text style={styles.itemCount}>
{cartItems.length} items
</Text>

{cartItems.length === 0 ? (
<Text style={styles.emptyCart}>
Drop products here to add to cart
</Text>
) : (
<ScrollView style={styles.cartItems}>
{cartItems.map(item => (
<CartItem key={item.id} item={item} />
))}
</ScrollView>
)}
</Droppable>
);
}

Aligned Drop Zones Grid​

function AlignedDropZones() {
const alignments = [
'top-left', 'top-center', 'top-right',
'center-left', 'center', 'center-right',
'bottom-left', 'bottom-center', 'bottom-right'
];

return (
<View style={styles.grid}>
{alignments.map(alignment => (
<Droppable
key={alignment}
onDrop={(data) => console.log(`Dropped at ${alignment}:`, data)}
dropAlignment={alignment}
dropOffset={{ x: 5, y: 5 }}
activeStyle={{
backgroundColor: 'rgba(59, 130, 246, 0.2)',
borderColor: '#3b82f6'
}}
style={styles.alignedZone}
>
<Text style={styles.alignmentLabel}>
{alignment}
</Text>
</Droppable>
))}
</View>
);
}

TypeScript Support​

The component is fully typed with generic support:

interface ProductData {
id: string;
name: string;
price: number;
category: string;
}

function TypedDroppable() {
return (
<Droppable<ProductData>
onDrop={(data: ProductData) => {
// data is properly typed
console.log(`Dropped product: ${data.name} - $${data.price}`);
}}
>
<Text>Product drop zone</Text>
</Droppable>
);
}

Performance Tips​

  1. Use useCallback for event handlers to prevent unnecessary re-renders
  2. Memoize expensive calculations in drop handlers
  3. Throttle visual feedback updates for better performance
  4. Limit capacity for large lists to prevent performance issues

Accessibility​

  • The component automatically handles accessibility for drop operations
  • Provide meaningful content and labels for screen readers
  • Consider alternative interaction methods for users with disabilities
  • Use semantic elements and proper ARIA labels

See Also​