Skip to main content

SortableItem

The SortableItem component represents individual items within a sortable list, providing drag-and-drop functionality, gesture handling, and smooth reordering animations.

Overview

SortableItem provides the drag-and-drop functionality for individual list items within a Sortable component. It handles gesture recognition, position animations, reordering logic, and can be used with or without drag handles for different interaction patterns.

Basic Usage

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

function TaskItem({ task, positions, ...sortableProps }) {
return (
<SortableItem
id={task.id}
data={task}
positions={positions}
{...sortableProps}
>
<View style={styles.taskContainer}>
<Text style={styles.taskTitle}>{task.title}</Text>
<Text style={styles.taskStatus}>
{task.completed ? 'Done' : 'Pending'}
</Text>
</View>
</SortableItem>
);
}

Props Reference

Core Props

PropTypeDefaultDescription
idstringRequiredUnique identifier for the item
positionsSharedValue<number[]>RequiredShared value for item positions
childrenReactNodeRequiredContent to render inside the item
dataT-Data associated with this item

Layout Props

PropTypeDefaultDescription
itemHeightnumberRequiredHeight of the item in pixels
itemsCountnumberRequiredTotal number of items in the list
containerHeightSharedValue<number>RequiredHeight of the container
lowerBoundSharedValue<number>RequiredLower scroll boundary
autoScrollDirectionSharedValue<ScrollDirection>RequiredAuto-scroll direction state

Styling Props

PropTypeDefaultDescription
styleStyleProp<ViewStyle>-Style for the item container
animatedStyleAnimatedStyleProp<ViewStyle>-Animated styles for the item

Callback Props

PropTypeDefaultDescription
onMove(id: string, from: number, to: number) => void-Called when item is moved
onDragStart(id: string, position: number) => void-Called when dragging starts
onDrop(id: string, position: number) => void-Called when dragging ends
onDragging(id: string, overItemId?: string, yPosition?: number) => void-Called during dragging

Examples

Basic Sortable Item

function TaskItem({ task, positions, ...sortableProps }) {
return (
<SortableItem
id={task.id}
data={task}
positions={positions}
{...sortableProps}
onMove={(id, from, to) => {
console.log(`Task ${id} moved from ${from} to ${to}`);
reorderTasks(id, from, to);
}}
>
<View style={styles.taskContainer}>
<Text style={styles.taskTitle}>{task.title}</Text>
<Text style={styles.taskStatus}>
{task.completed ? 'Done' : 'Pending'}
</Text>
</View>
</SortableItem>
);
}

const styles = StyleSheet.create({
taskContainer: {
backgroundColor: 'white',
padding: 16,
marginHorizontal: 16,
marginVertical: 4,
borderRadius: 8,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.1,
shadowRadius: 2,
elevation: 2,
},
taskTitle: {
fontSize: 16,
fontWeight: '500',
flex: 1,
},
taskStatus: {
fontSize: 14,
color: '#666',
},
});

Sortable Item with Drag Handle

function TaskItemWithHandle({ task, positions, ...sortableProps }) {
return (
<SortableItem
id={task.id}
data={task}
positions={positions}
{...sortableProps}
>
<View style={styles.taskContainer}>
<View style={styles.taskContent}>
<Text style={styles.taskTitle}>{task.title}</Text>
<Text style={styles.taskDescription}>{task.description}</Text>
</View>

{/* Only this handle can initiate dragging */}
<SortableItem.Handle style={styles.dragHandle}>
<View style={styles.handleIcon}>
<View style={styles.handleLine} />
<View style={styles.handleLine} />
<View style={styles.handleLine} />
</View>
</SortableItem.Handle>
</View>
</SortableItem>
);
}

const styles = StyleSheet.create({
taskContainer: {
backgroundColor: 'white',
padding: 16,
marginHorizontal: 16,
marginVertical: 4,
borderRadius: 8,
flexDirection: 'row',
alignItems: 'center',
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.1,
shadowRadius: 2,
elevation: 2,
},
taskContent: {
flex: 1,
},
taskTitle: {
fontSize: 16,
fontWeight: '600',
marginBottom: 4,
},
taskDescription: {
fontSize: 14,
color: '#666',
},
dragHandle: {
padding: 8,
marginLeft: 12,
},
handleIcon: {
width: 20,
height: 20,
justifyContent: 'space-between',
alignItems: 'center',
},
handleLine: {
width: 16,
height: 2,
backgroundColor: '#999',
borderRadius: 1,
},
});

Advanced Item with State Tracking

function AdvancedTaskItem({ task, positions, ...sortableProps }) {
const [isDragging, setIsDragging] = useState(false);
const [isHovered, setIsHovered] = useState(false);

return (
<SortableItem
id={task.id}
data={task}
positions={positions}
{...sortableProps}
onDragStart={(id, position) => {
setIsDragging(true);
hapticFeedback();
analytics.track('drag_start', { taskId: id, position });
}}
onDrop={(id, position) => {
setIsDragging(false);
analytics.track('drag_end', { taskId: id, position });
}}
onDragging={(id, overItemId, yPosition) => {
if (overItemId) {
// Show visual feedback for item being hovered over
setIsHovered(overItemId === task.id);
}
}}
onMove={(id, from, to) => {
// Update data and sync to backend
reorderTasks(id, from, to);
syncToBackend();
}}
style={[
styles.taskItem,
isDragging && styles.draggingItem,
isHovered && styles.hoveredItem
]}
>
<View style={styles.taskContent}>
<Text style={styles.taskTitle}>{task.title}</Text>
<Text style={styles.taskPriority}>Priority: {task.priority}</Text>
<Text style={styles.taskDue}>Due: {task.dueDate}</Text>
{isDragging && (
<Text style={styles.dragIndicator}>Dragging...</Text>
)}
</View>

<View style={styles.taskMeta}>
<Text style={styles.taskAssignee}>{task.assignee}</Text>
<View style={[styles.priorityDot, getPriorityColor(task.priority)]} />
</View>
</SortableItem>
);
}

const styles = StyleSheet.create({
taskItem: {
backgroundColor: 'white',
borderRadius: 8,
padding: 16,
marginHorizontal: 16,
marginVertical: 4,
flexDirection: 'row',
alignItems: 'center',
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.1,
shadowRadius: 2,
elevation: 2,
},
draggingItem: {
shadowOpacity: 0.3,
shadowRadius: 8,
elevation: 8,
transform: [{ scale: 1.02 }],
backgroundColor: '#f8f9fa',
},
hoveredItem: {
backgroundColor: 'rgba(59, 130, 246, 0.05)',
borderColor: '#3b82f6',
borderWidth: 1,
},
taskContent: {
flex: 1,
},
taskTitle: {
fontSize: 16,
fontWeight: '600',
marginBottom: 4,
},
taskPriority: {
fontSize: 12,
color: '#666',
marginBottom: 2,
},
taskDue: {
fontSize: 12,
color: '#999',
},
dragIndicator: {
fontSize: 12,
color: '#3b82f6',
fontWeight: 'bold',
marginTop: 4,
},
taskMeta: {
alignItems: 'flex-end',
},
taskAssignee: {
fontSize: 12,
color: '#666',
marginBottom: 4,
},
priorityDot: {
width: 8,
height: 8,
borderRadius: 4,
},
});

function getPriorityColor(priority) {
switch (priority) {
case 'high':
return { backgroundColor: '#ef4444' };
case 'medium':
return { backgroundColor: '#f59e0b' };
case 'low':
return { backgroundColor: '#10b981' };
default:
return { backgroundColor: '#6b7280' };
}
}

Custom Animated Styles

function AnimatedTaskItem({ task, positions, ...sortableProps }) {
const scale = useSharedValue(1);
const opacity = useSharedValue(1);

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

return (
<SortableItem
id={task.id}
data={task}
positions={positions}
{...sortableProps}
animatedStyle={animatedStyle}
onDragStart={() => {
scale.value = withSpring(1.05);
opacity.value = withTiming(0.9);
}}
onDrop={() => {
scale.value = withSpring(1);
opacity.value = withTiming(1);
}}
>
<View style={styles.taskItem}>
<Text style={styles.taskTitle}>{task.title}</Text>
</View>
</SortableItem>
);
}

File List Item

function FileListItem({ file, positions, ...sortableProps }) {
const [isSelected, setIsSelected] = useState(false);

return (
<SortableItem
id={file.id}
data={file}
positions={positions}
{...sortableProps}
onMove={(id, from, to) => {
reorderFiles(id, from, to);
showToast('File reordered');
}}
>
<Pressable
style={[styles.fileItem, isSelected && styles.selectedFile]}
onPress={() => setIsSelected(!isSelected)}
>
<View style={styles.fileIcon}>
<Icon name={getFileIcon(file.type)} size={24} color={getFileColor(file.type)} />
</View>

<View style={styles.fileInfo}>
<Text style={styles.fileName}>{file.name}</Text>
<Text style={styles.fileSize}>{formatFileSize(file.size)}</Text>
<Text style={styles.fileDate}>{formatDate(file.modifiedDate)}</Text>
</View>

<SortableItem.Handle style={styles.dragHandle}>
<Icon name="drag-handle" size={20} color="#999" />
</SortableItem.Handle>
</Pressable>
</SortableItem>
);
}

const styles = StyleSheet.create({
fileItem: {
backgroundColor: 'white',
padding: 12,
marginHorizontal: 16,
marginVertical: 2,
borderRadius: 6,
flexDirection: 'row',
alignItems: 'center',
borderWidth: 1,
borderColor: '#e5e7eb',
},
selectedFile: {
backgroundColor: '#eff6ff',
borderColor: '#3b82f6',
},
fileIcon: {
marginRight: 12,
},
fileInfo: {
flex: 1,
},
fileName: {
fontSize: 14,
fontWeight: '500',
marginBottom: 2,
},
fileSize: {
fontSize: 12,
color: '#666',
},
fileDate: {
fontSize: 11,
color: '#999',
},
dragHandle: {
padding: 8,
marginLeft: 8,
},
});

Handle Component

The SortableItem.Handle component creates specific draggable areas within the item:

Handle Props

PropTypeDescription
childrenReactNodeContent to render inside the handle
styleStyleProp<ViewStyle>Style for the handle container

Handle Examples

// Icon handle
<SortableItem.Handle style={styles.iconHandle}>
<Icon name="drag-handle" size={20} color="#666" />
</SortableItem.Handle>

// Custom dots handle
<SortableItem.Handle style={styles.dotsHandle}>
<View style={styles.dotsGrid}>
{Array.from({ length: 6 }).map((_, i) => (
<View key={i} style={styles.dot} />
))}
</View>
</SortableItem.Handle>

// Lines handle
<SortableItem.Handle style={styles.linesHandle}>
<View style={styles.linesContainer}>
<View style={styles.line} />
<View style={styles.line} />
<View style={styles.line} />
</View>
</SortableItem.Handle>

// Text handle
<SortableItem.Handle style={styles.textHandle}>
<Text style={styles.handleText}></Text>
</SortableItem.Handle>

Handle Styling

const handleStyles = StyleSheet.create({
iconHandle: {
padding: 8,
borderRadius: 4,
backgroundColor: '#f3f4f6',
},
dotsHandle: {
padding: 8,
},
dotsGrid: {
width: 16,
height: 16,
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'space-between',
alignContent: 'space-between',
},
dot: {
width: 2,
height: 2,
backgroundColor: '#9ca3af',
borderRadius: 1,
},
linesHandle: {
padding: 8,
},
linesContainer: {
width: 16,
height: 12,
justifyContent: 'space-between',
},
line: {
height: 2,
backgroundColor: '#9ca3af',
borderRadius: 1,
},
textHandle: {
padding: 8,
},
handleText: {
fontSize: 16,
color: '#9ca3af',
fontWeight: 'bold',
},
});

Callback Details

onMove Callback

Called when the item is successfully moved to a new position:

const handleMove = (itemId: string, fromIndex: number, toIndex: number) => {
console.log(`Item ${itemId} moved from position ${fromIndex} to ${toIndex}`);

// Update your data array
const newData = [...data];
const [movedItem] = newData.splice(fromIndex, 1);
newData.splice(toIndex, 0, movedItem);
setData(newData);

// Sync to backend
updateItemOrder(itemId, toIndex);
};

onDragStart Callback

Called when dragging begins:

const handleDragStart = (itemId: string, position: number) => {
console.log(`Started dragging item ${itemId} at position ${position}`);

// Haptic feedback
hapticFeedback();

// Update UI state
setDraggedItemId(itemId);

// Analytics
analytics.track('sortable_drag_start', { itemId, position });
};

onDrop Callback

Called when dragging ends:

const handleDrop = (itemId: string, position: number) => {
console.log(`Dropped item ${itemId} at position ${position}`);

// Clear UI state
setDraggedItemId(null);

// Success feedback
showToast('Item reordered successfully');

// Analytics
analytics.track('sortable_drag_end', { itemId, position });
};

onDragging Callback

Called continuously during dragging:

const handleDragging = (
itemId: string,
overItemId?: string,
yPosition?: number
) => {
console.log(`Dragging ${itemId}, over ${overItemId}, at y: ${yPosition}`);

// Update hover states
if (overItemId) {
setHoveredItemId(overItemId);
}

// Update drag position for overlays
if (yPosition !== undefined) {
updateDragOverlayPosition(yPosition);
}
};

TypeScript Support

The SortableItem component is fully typed with generic support:

interface TaskData {
id: string;
title: string;
priority: 'low' | 'medium' | 'high';
completed: boolean;
assignee?: string;
}

// Fully typed sortable item
<SortableItem<TaskData>
id={task.id}
data={task}
positions={positions}
{...props}
onMove={(id: string, from: number, to: number) => {
// Fully typed callback
reorderTasks(id, from, to);
}}
>
<TaskComponent task={task} />
</SortableItem>

Performance Tips

  • Use React.memo for item content that doesn't change frequently
  • Avoid heavy computations in drag callbacks
  • Use useCallback for stable callback references
  • Keep item heights consistent for better performance
  • Minimize the number of animated styles

Accessibility

The SortableItem component supports accessibility features:

<SortableItem
id={item.id}
data={item}
positions={positions}
{...props}
>
<View
accessible={true}
accessibilityRole="button"
accessibilityLabel={`Sortable item: ${item.title}`}
accessibilityHint="Double tap and hold to reorder this item"
accessibilityActions={[
{ name: 'move-up', label: 'Move up' },
{ name: 'move-down', label: 'Move down' },
]}
onAccessibilityAction={(event) => {
if (event.nativeEvent.actionName === 'move-up') {
moveItemUp(item.id);
} else if (event.nativeEvent.actionName === 'move-down') {
moveItemDown(item.id);
}
}}
>
<Text>{item.title}</Text>
</View>
</SortableItem>

Common Patterns

Conditional Handles

function ConditionalHandleItem({ item, canReorder, ...props }) {
return (
<SortableItem {...props}>
<View style={styles.item}>
<Text>{item.title}</Text>
{canReorder && (
<SortableItem.Handle style={styles.handle}>
<Icon name="drag-handle" />
</SortableItem.Handle>
)}
</View>
</SortableItem>
);
}

Multi-select Support

function MultiSelectItem({ item, isSelected, onSelect, ...props }) {
return (
<SortableItem {...props}>
<Pressable
style={[styles.item, isSelected && styles.selected]}
onPress={() => onSelect(item.id)}
>
<Checkbox value={isSelected} />
<Text>{item.title}</Text>
<SortableItem.Handle style={styles.handle}>
<Icon name="drag-handle" />
</SortableItem.Handle>
</Pressable>
</SortableItem>
);
}

See Also