react-native-swappable-grid
v1.0.13
Published
A React Native component for creating draggable, swappable grid layouts with reordering, delete functionality, and smooth animations
Downloads
1,673
Maintainers
Readme
react-native-swappable-grid
A powerful React Native component for creating draggable, swappable grid layouts with smooth animations, reordering, and delete functionality.
Features
- 🎯 Drag & Drop: Long press to drag and reorder items in a grid layout
- 📐 Flexible Layout: Automatic column calculation or fixed number of columns
- 🎨 Smooth Animations: Optional wiggle animation during drag mode
- 🗑️ Delete Support: Built-in hold-still-to-delete or custom delete component (trashcan)
- ➕ Trailing Components: Support for additional components (e.g., "Add" button)
- 👋 Haptic Feedback: Optional haptic feedback on deletion with Vibration API, or optionally (better) expo-haptics
- 📜 Auto-scroll: Automatic scrolling when dragging near edges
- 🔄 Order Tracking: Callbacks for order changes and drag end events
- ⚡ Performance: Built with React Native Reanimated and Gesture Handler for 60fps animations
📱 Demo
Example Project
To see common usages and examples. Check out the example project 🚀
Installation
npm install react-native-swappable-grid
# or
yarn add react-native-swappable-gridPeer Dependencies
This package requires the following peer dependencies:
npm install react react-native react-native-gesture-handler react-native-reanimated
# or
yarn add react react-native react-native-gesture-handler react-native-reanimatedOptional: For better haptic feedback (especially on iOS), install expo-haptics:
npm install expo-haptics
# or
yarn add expo-hapticsNote:
expo-hapticsworks in both Expo and bare React Native projects. If not installed, the library will fall back to React Native's Vibration API (which has limited control on iOS and will give harsher vibrations).
Important: Make sure to follow the setup instructions for:
TLDR: Wrap your app with the GestureHandlerRootView inside RootLayout in _layout.tsx
Additional fix for now: Disable Strict Mode for Reanimated because of logger warnings did I did not manage to get rid of.
import { Slot } from "expo-router";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import {
configureReanimatedLogger,
ReanimatedLogLevel,
} from "react-native-reanimated";
// Strict mode is disabled because it gave warnings in SwappableGrid with useSharedValue()
configureReanimatedLogger({
level: ReanimatedLogLevel.warn, // Only log warnings & errors
strict: false, // Disable strict mode warnings
});
export default function RootLayout() {
return (
<GestureHandlerRootView>
<Slot />
</GestureHandlerRootView>
);
}Basic Usage
import React, { useState } from "react";
import { View, Text } from "react-native";
import { SwappableGrid } from "react-native-swappable-grid";
const MyComponent = () => {
const [items, setItems] = useState([
{ id: "1", title: "Item 1" },
{ id: "2", title: "Item 2" },
{ id: "3", title: "Item 3" },
]);
return (
<SwappableGrid
itemWidth={100}
itemHeight={100}
numColumns={3} /* leave excluded for dynamic columns */
gap={8}
onOrderChange={(keys) => {
// Reorder items based on new key order
const newOrder = keys
.map((key) => items.find((item) => item.id === key))
.filter(Boolean);
setItems(newOrder);
}}
>
{items.map((item) => (
<View
key={item.id}
style={{ backgroundColor: "#ccc", borderRadius: 8 }}
>
<Text>{item.title}</Text>
</View>
))}
</SwappableGrid>
);
};API Reference
SwappableGrid Props
| Prop | Type | Usage | Default | Description |
| ------------------------ | --------------------------------------- | -------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| children | ReactNode | Required | - | The child components to render in the grid. Each child should have a unique key. |
| itemWidth | number | Required | - | Width of each grid item in pixels |
| itemHeight | number | Required | - | Height of each grid item in pixels |
| gap | number | Optional | 8 | Gap between grid items in pixels |
| containerPadding | number | Optional | 8 | Padding around the container in pixels |
| holdToDragMs | number | Optional | 300 | Duration in milliseconds to hold before drag starts |
| numColumns | number | Optional | Auto | Number of columns in the grid. If not provided, will be calculated automatically based on container width |
| wiggle | { duration: number; degrees: number } | Optional | - | Wiggle animation configuration when items are in drag mode |
| wiggleDeleteMode | { duration: number; degrees: number } | Optional | - | Wiggle animation configuration specifically for delete mode. If not provided, uses 2x degrees and 0.7x duration of wiggle prop |
| holdStillToDeleteMs | number | Optional | 1000 | Duration in milliseconds to hold an item still before entering delete mode |
| hapticFeedback | boolean | Optional | false | Enable haptic feedback when entering delete mode. Uses expo-haptics if available (recommended for iOS), otherwise falls back to Vibration API. |
| onDragEnd | (ordered: ChildNode[]) => void | Optional | - | Callback fired when drag ends, providing the ordered array of child nodes |
| onOrderChange | (keys: string[]) => void | Optional | - | Callback fired when the order changes, providing an array of keys in the new order |
| onDelete | (key: string) => void | Optional | - | Callback fired when an item is deleted, providing the key of the deleted item |
| dragSizeIncreaseFactor | number | Optional | 1.06 | Factor by which the dragged item scales up |
| scrollSpeed | number | Optional | 10 | Speed of auto-scrolling when dragging near edges |
| scrollThreshold | number | Optional | 100 | Distance from edge in pixels that triggers auto-scroll |
| style | StyleProp<ViewStyle> | Optional | - | Custom style for the ScrollView container |
| trailingComponent | ReactNode | Optional | - | Component to render after all grid items (e.g., an "Add" button) |
| deleteComponent | ReactNode | Optional | - | Component to render as a delete target (shown when dragging), kind of like a trashcan feature. If provided, disables hold-still-to-delete feature |
| deleteComponentStyle | StyleProp<ViewStyle> | Optional | - | Custom style for the delete component. If provided, allows custom positioning |
| reverse | boolean | Optional | false | If true, reverses the order of items (right-to-left, bottom-to-top) |
SwappableGridRef
The component can be used with a ref to access imperative methods:
Very useful interactive feature for canceling deleteMode when clicking outside the grid. See example project for example usage.
import { StyleSheet, Pressable} from "react-native";
import { SwappableGrid, SwappableGridRef } from "react-native-swappable-grid";
const gridRef = useRef<SwappableGridRef>(null);
return (
<Pressable
style={StyleSheet.absoluteFill}
onPress={() => {
// Cancel delete mode when user taps outside the grid
if (gridRef.current) {
gridRef.current.cancelDeleteMode();
}
}}
>
<SwappableGrid ref={gridRef} ... />
</Pressable>
)| Method | Description |
| -------------------- | --------------------------------------------------------------- |
| cancelDeleteMode() | Cancels the delete mode if any item is currently in delete mode |
Examples
With Wiggle Animation
<SwappableGrid
itemWidth={100}
itemHeight={100}
numColumns={3} /* leave excluded for dynamic columns */
wiggle={{ duration: 200, degrees: 3 }}
onOrderChange={(keys) => console.log("New order:", keys)}
>
{items.map((item) => (
<View key={item.id}>{item.content}</View>
))}
</SwappableGrid>Auto-calculated Columns
<SwappableGrid
itemWidth={100}
itemHeight={100}
gap={12}
containerPadding={16}
// numColumns not provided - will be calculated automatically
onOrderChange={(keys) => console.log("New order:", keys)}
>
{items.map((item) => (
<View key={item.id}>{item.content}</View>
))}
</SwappableGrid>With Trailing Component (Add Button)
<SwappableGrid
itemWidth={100}
itemHeight={100}
numColumns={3}
trailingComponent={
/* trailingComponent gets positioned at the end of the grid */
<Pressable
onPress={() => addNewItem()}
style={{
backgroundColor: "#007AFF",
borderRadius: 8,
justifyContent: "center",
alignItems: "center",
}}
>
<Text style={{ color: "white", fontSize: 24 }}>+</Text>
</Pressable>
}
>
{items.map((item) => (
<View key={item.id}>{item.content}</View>
))}
</SwappableGrid>With Delete Functionality (Hold-still-to-Delete)
Note: Hold-still-to-delete is only enabled when onDelete is provided. If you don't want deletion functionality, simply omit the onDelete prop.
<SwappableGrid
itemWidth={100}
itemHeight={100}
numColumns={3}
holdStillToDeleteMs={1500} // Hold for 1.5 seconds to enter delete mode
onDelete={(key) => {
setItems(items.filter((item) => item.id !== key));
}}
>
{items.map((item) => (
<View key={item.id}>{item.content}</View>
))}
</SwappableGrid>With Delete Component (custom component where you can drag items into to delete. Kind of like a trashcan)
Note: By default HoldToDelete is enabled if you have a onDelete prop. If you use DeleteComponent then only DeleteComponent is used as the way to delete. In other words. You can choose to either delete items by holding or by using DeleteComponent.
<SwappableGrid
itemWidth={100}
itemHeight={100}
numColumns={3}
deleteComponent={
<View
style={{
backgroundColor: "red",
borderRadius: 8,
justifyContent: "center",
alignItems: "center",
}}
>
<Text style={{ color: "white" }}>Drop to Delete</Text>
</View>
}
deleteComponentStyle={{
/* By default DeleteComponent acts like the TrailingComponent. Positioning itself at the end of the grid. By using deleteComponentStyle you can change it to have a static position if you want it to be at a specific place */
position: "absolute",
top: 20,
right: 20,
width: 100,
height: 100,
}}
onDelete={(key) => {
setItems(items.filter((item) => item.id !== key));
}}
>
{items.map((item) => (
<View key={item.id}>{item.content}</View>
))}
</SwappableGrid>How It Works
- Long Press: Hold an item for
holdToDragMsmilliseconds to enter drag mode - Drag: Move the item to swap positions with other items
- Auto-scroll: When dragging near edges, the grid automatically scrolls
- Delete:
- Hold-Still-to-delete: Hold an item still for
holdStillToDeleteMs(default: 1000ms) to enter delete mode (shows delete button). Only enabled whenonDeleteis provided. - Delete component: Drag an item to the delete component to delete it
- Hold-Still-to-delete: Hold an item still for
- Order Change: The
onOrderChangecallback fires whenever items are reordered
Notes
- Each child component must have a unique
keyprop - The component uses
react-native-reanimatedfor smooth 60fps animations - Deletion is only enabled when
onDeleteis provided. By default hold-still-to-delete is selected delete method. IfdeleteComponentis provided, hold-still-to-delete is automatically disabled anddeleteComponentis used as the way to delete. - The trailing component is positioned after all grid items
- The delete component appears only when dragging (if provided)
- If
wiggleDeleteModeis not provided, delete mode wiggle uses 2x degrees and 0.7x duration of thewiggleprop
License
ISC
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
Contact
For any question regarding the package, feel free to contact me at [email protected]
Cheers! ✨
