npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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

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-grid

Peer 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-reanimated

Optional: For better haptic feedback (especially on iOS), install expo-haptics:

npm install expo-haptics
# or
yarn add expo-haptics

Note: expo-haptics works 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

  1. Long Press: Hold an item for holdToDragMs milliseconds to enter drag mode
  2. Drag: Move the item to swap positions with other items
  3. Auto-scroll: When dragging near edges, the grid automatically scrolls
  4. Delete:
    • Hold-Still-to-delete: Hold an item still for holdStillToDeleteMs (default: 1000ms) to enter delete mode (shows delete button). Only enabled when onDelete is provided.
    • Delete component: Drag an item to the delete component to delete it
  5. Order Change: The onOrderChange callback fires whenever items are reordered

Notes

  • Each child component must have a unique key prop
  • The component uses react-native-reanimated for smooth 60fps animations
  • Deletion is only enabled when onDelete is provided. By default hold-still-to-delete is selected delete method. If deleteComponent is provided, hold-still-to-delete is automatically disabled and deleteComponent is 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 wiggleDeleteMode is not provided, delete mode wiggle uses 2x degrees and 0.7x duration of the wiggle prop

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! ✨