react-native-dragflow
v0.1.0
Published
Gesture-driven, auto-scrolling draggable FlatList for React Native — vertical & horizontal — built from scratch on Reanimated 4 and Gesture Handler.
Downloads
153
Maintainers
Readme
react-native-dragflow
A gesture-driven, auto-scrolling draggable FlatList for React Native — works vertically and horizontally, reorders state for you, and is built entirely from scratch on Reanimated 4 and React Native Gesture Handler. No native modules of its own; everything runs on the UI thread.
Features
- 🟰 Vertical and horizontal drag-to-reorder from a single component.
- 🪄 Auto-scroll at the edges — drag an item toward the edge and the list scrolls, with speed that ramps up the closer you get.
- 🔁 Automatic reordering — pass a
setListData(or listen viaonChange) and the new order is applied for you. - 🎛️ Tunable via props — the edge trigger zone and the max scroll speed are both yours to set.
- 👻 Ghost overlay — the lifted item renders as a floating copy you can style independently via a
highlightflag. - 🧵 Runs on the UI thread — gestures, the ghost, the auto-scroll loop, and neighbour shifting are all Reanimated worklets, so dragging stays smooth.
- 🧩 Forwards
FlatListprops — anything you'd normally pass to aFlatList(minus the props the library controls) is passed straight through.
Preview

Installation
npm install react-native-dragflow
# or
yarn add react-native-dragflowPeer dependencies
This library is a thin layer over Reanimated and Gesture Handler, so you install those in your app:
npm install react-native-reanimated react-native-worklets react-native-gesture-handler| Peer dependency | Required version |
| ------------------------- | ---------------- |
| react | >= 18 |
| react-native | >= 0.80 |
| react-native-reanimated | >= 4.0 |
| react-native-worklets | >= 0.7 |
If
npm installfails withERESOLVE, your app has a pre-existing peer-dependency conflict between Reanimated and Worklets (or another package). This is common in RN projects and is not specific to this library. Install with--legacy-peer-deps:npm install react-native-dragflow --legacy-peer-deps|
react-native-gesture-handler|>= 2.9|
Reanimated 4 only. This library uses
scheduleOnRNfromreact-native-worklets, which is Reanimated 4's replacement forrunOnJS. Reanimated 4 supports the New Architecture only — if your app is still on Reanimated 3 / the old architecture, stay on a Reanimated-3 draggable list instead.
Setup
Two one-time steps, both standard for any Reanimated 4 + Gesture Handler app.
1. Add the Worklets babel plugin (must be the last plugin) in babel.config.js:
module.exports = {
presets: ["module:@react-native/babel-preset"],
plugins: [
// ...your other plugins
"react-native-worklets/plugin", // keep this last
],
};2. Wrap your app root in GestureHandlerRootView (the drag gestures won't fire without it):
import { GestureHandlerRootView } from "react-native-gesture-handler";
export default function App() {
return (
<GestureHandlerRootView style={{ flex: 1 }}>
{/* ...your app */}
</GestureHandlerRootView>
);
}Then rebuild the app (npx react-native run-android / run-ios) — a JS reload isn't enough after adding the babel plugin.
Quick start (vertical)
Press and hold an item for ~300ms to pick it up, then drag to reorder. Drag toward the top or bottom edge to auto-scroll.
import React, { useState } from "react";
import { View, Text, StyleSheet } from "react-native";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { DraggableList } from "react-native-dragflow";
type Item = { id: number; label: string };
const initialData: Item[] = Array.from({ length: 20 }, (_, i) => ({
id: i + 1,
label: `Item ${i + 1}`,
}));
export default function App() {
const [data, setData] = useState<Item[]>(initialData);
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<DraggableList<Item>
listData={data}
setListData={setData}
orientation="vertical"
separatorGap={8}
keyExtractor={(item) => String(item.id)}
renderDataItem={({ item, highlight }) => (
<View style={[styles.card, highlight && styles.lifted]}>
<Text style={styles.label}>{item.label}</Text>
</View>
)}
/>
</GestureHandlerRootView>
);
}
const styles = StyleSheet.create({
card: { padding: 16, borderRadius: 12, backgroundColor: "#f2f2f7" },
lifted: { backgroundColor: "#fff", borderWidth: 2, borderColor: "#ff3b30" },
label: { fontSize: 16 },
});Horizontal list
Set orientation="horizontal" and give items a fixed width with horizontalItemSize:
<DraggableList<Item>
listData={data}
setListData={setData}
orientation="horizontal"
horizontalItemSize={120}
separatorGap={12}
keyExtractor={(item) => String(item.id)}
showsHorizontalScrollIndicator={false}
renderDataItem={({ item, highlight }) => (
<View style={[styles.chip, highlight && styles.lifted]}>
<Text>{item.label}</Text>
</View>
)}
/>Reacting to reorders with onChange
If your data lives outside local state (Redux, Zustand, a server sync, etc.), skip setListData and use onChange — it fires with the freshly reordered array every time a drag commits:
<DraggableList<Item>
listData={data}
orientation="vertical"
separatorGap={8}
keyExtractor={(item) => String(item.id)}
onChange={(newData) => {
persistOrder(newData); // your store / API call
}}
renderDataItem={({ item }) => <Row item={item} />}
/>Provide at least one of
setListDataoronChange— otherwise the new order has nowhere to go and the list snaps back tolistDataon release.
Props
DraggableList takes the props below, plus any other FlatList prop (those are forwarded to the underlying list). The forwarded set excludes data, renderItem, ItemSeparatorComponent, and horizontal, which the library manages internally.
| Prop | Type | Required | Default | Description |
| -------------------- | ------------------------------------ | :------: | :-----: | ----------------------------------------------------------------------------------------------------- |
| listData | T[] | ✅ | — | The array of items to render. |
| orientation | 'vertical' \| 'horizontal' | ✅ | — | Direction of the list and the drag. |
| keyExtractor | (item: T, index: number) => string | ✅ | — | Returns a stable, unique key per item. |
| renderDataItem | ({ item, highlight }) => ReactNode | ✅ | — | Renders an item. highlight is true only for the floating ghost (see below). |
| separatorGap | number | ✅ | — | Gap in px between items. Also rendered as the list's separator and factored into the reorder spacing. |
| setListData | Dispatch<SetStateAction<T[]>> | — | — | Your useState setter. The library calls it with the reordered array. |
| onChange | (newData: T[]) => void | — | — | Called with the reordered array after each drag commits. |
| dragEnabled | boolean | — | true | Turn dragging on/off without unmounting the list. |
| autoScrollEdge | number | — | 120 | Size in px of the edge zone that triggers auto-scroll. |
| maxSpeed | number | — | 12 | Max auto-scroll speed in px per frame. |
| horizontalItemSize | number | — | 100 | Fixed item width in px. Used only when orientation="horizontal". |
| ...rest | FlatList props | — | — | Forwarded to the underlying Animated.FlatList. |
The highlight flag
renderDataItem is called for two things: the items sitting in the list (highlight: false) and the ghost — the floating copy that follows your finger while dragging (highlight: true). Use it to make the lifted item look picked-up:
renderDataItem={({ item, highlight }) => (
<View style={[styles.card, highlight && styles.lifted]}>
<Text>{item.label}</Text>
</View>
)}Tuning auto-scroll
autoScrollEdge and maxSpeed together control the edge behaviour:
autoScrollEdge— how far from the edge (in px) the auto-scroll zone reaches. Larger = scrolling kicks in sooner; smaller = you have to drag closer to the edge.maxSpeed— the fastest the list will scroll (px per frame, ~60fps). The actual speed ramps from 0 up to this cap based on how deep into the edge zone the item's centre is.
<DraggableList
autoScrollEdge={150} // wider trigger zone
maxSpeed={20} // faster scroll at the edge
/* ... */
/>How it works
- Pick up: a
Pangesture activates after a ~300ms long-press, so normal taps and scrolls aren't hijacked. - Ghost overlay: on pick-up the original item fades out and an absolutely-positioned copy (
highlight: true) is rendered on top, following your finger. - Neighbour shift: as you drag, the library computes how many slots you've crossed (a "step" coefficient) and springs the other items out of the way to preview the drop position.
- Edge auto-scroll: a frame callback continuously scrolls the underlying list while the dragged item sits in the edge zone, and the drop target keeps tracking correctly even as the content scrolls underneath.
- Commit: on release the array is spliced into its new order and handed back through
setListData/onChange, the ghost fades out, and all shared values reset.
Everything except the final state update runs as Reanimated worklets on the UI thread; the state update is marshalled back with scheduleOnRN.
Requirements & limitations
Worth knowing before you wire it in:
- Uniform item size. The reorder math measures one item and reuses that "step" for the whole list. All items must be the same size along the scroll axis (same height for vertical, same width for horizontal). Variable-height rows aren't supported yet.
- New Architecture only, because it depends on Reanimated 4.
GestureHandlerRootViewis required at the app root.- Long-press delay is currently fixed at 300ms and isn't yet exposed as a prop.
Known issues
These are known limitations in 0.1.0 and tracked for future releases:
- Ghost overlay drifts on long lists (Android). With ~100+ items, the floating preview can appear slightly offset from the item being dragged. The reorder logic is unaffected — only the visual position of the ghost is wrong. iOS is not affected.
- Fast drags can release early (Android). A very quick pickup-and-drag can cause the gesture to release before the drop position is finalized, dropping the item mid-list instead of where intended. iOS is not affected. A deliberate drag works correctly.
- Uniform item sizes only. See Requirements & limitations — variable-sized items are not yet supported.
Issues and PRs welcome — see Contributing.
Troubleshooting
| Symptom | Likely cause |
| ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| Nothing happens on long-press | App isn't wrapped in GestureHandlerRootView, or the react-native-worklets/plugin babel plugin is missing / not last. Rebuild after adding it. |
| scheduleOnRN / worklets error on launch | react-native-worklets isn't installed, or you're on Reanimated 3. This library needs Reanimated 4. |
| Items overlap or drop into the wrong slot | Item sizes aren't uniform along the scroll axis (see limitations), or separatorGap doesn't match the actual spacing between items. |
| Ghost is misaligned in a horizontal list | horizontalItemSize doesn't match the rendered item width. |
Contributing
Issues and PRs welcome. To work on the library locally:
npm install
npm run typecheck # type-check the source
npm run prepare # build lib/ with react-native-builder-bobLicense
MIT © Harsh Mer
