Gathering detailed insights and metrics for @osamaq/drag-select
Gathering detailed insights and metrics for @osamaq/drag-select
Gathering detailed insights and metrics for @osamaq/drag-select
Gathering detailed insights and metrics for @osamaq/drag-select
👆 A React Native utility for creating a pan gesture that auto-selects items in a list, like your favorite gallery app.
npm install @osamaq/drag-select
Typescript
Module System
Min. Node Version
Node Version
NPM Version
TypeScript (97.15%)
JavaScript (2.85%)
Total Downloads
0
Last Day
0
Last Week
0
Last Month
0
Last Year
0
MIT License
120 Stars
70 Commits
2 Forks
1 Watchers
1 Branches
1 Contributors
Updated on May 28, 2025
Latest Version
0.2.0
Package Id
@osamaq/drag-select@0.2.0
Unpacked Size
129.10 kB
Size
29.20 kB
File Count
47
NPM Version
10.9.2
Node Version
22.13.1
Published on
Feb 02, 2025
Cumulative downloads
Total Downloads
Last Day
0%
NaN
Compared to previous day
Last Week
0%
NaN
Compared to previous week
Last Month
0%
NaN
Compared to previous month
Last Year
0%
NaN
Compared to previous year
4
A utility for creating a pan gesture that auto-selects items in a list, like your favorite gallery app.
ScrollView
, FlatList
, FlashList
etc.[!TIP] Try it out in an Expo Snack
[!IMPORTANT] This package requires Reanimated v3 and Gesture Handler v2.
1npm install @osamaq/drag-select
1yarn add @osamaq/drag-select
1pnpm add @osamaq/drag-select
This package is made with Reanimated & Gesture Handler, and using it requires some familiarity.
useDragSelect
is a utility hook. It works by taking in parameters describing the UI of your list and returns managed gestures.
Paste this snippet into your project to get started.
1import { useDragSelect } from "@osamaq/drag-select" 2 3import { FlatList, View, Text } from "react-native" 4import { GestureDetector } from "react-native-gesture-handler" 5import Animated, { useAnimatedRef, useAnimatedScrollHandler } from "react-native-reanimated" 6 7function List() { 8 const data = Array.from({ length: 50 }).map((_, index) => ({ 9 id: `usr_${index}`, 10 name: "foo", 11 })) 12 13 const flatlist = useAnimatedRef<FlatList<(typeof data)[number]>>() 14 15 const { gestures, onScroll } = useDragSelect({ 16 data, 17 key: "id", 18 list: { 19 animatedRef: flatlist, 20 numColumns: 2, 21 itemSize: { height: 50, width: 50 }, 22 }, 23 onItemSelected: (id, index) => { 24 console.log("onItemSelected", { id, index }) 25 }, 26 onItemDeselected: (id, index) => { 27 console.log("onItemDeselected", { id, index }) 28 }, 29 onItemPress: (id, index) => { 30 console.log("onItemPress", { id, index }) 31 }, 32 }) 33 34 const scrollHandler = useAnimatedScrollHandler({ onScroll }) 35 36 return ( 37 <GestureDetector gesture={gestures.panHandler}> 38 <Animated.FlatList 39 data={data} 40 ref={flatlist} 41 numColumns={2} 42 onScroll={scrollHandler} 43 renderItem={({ item, index }) => ( 44 <GestureDetector gesture={gestures.createItemPressHandler(item.id, index)}> 45 <View style={{ width: 50, height: 50, backgroundColor: "salmon" }}> 46 <Text>{item.id}</Text> 47 </View> 48 </GestureDetector> 49 )} 50 /> 51 </GestureDetector> 52 ) 53}
Check out the step-by-step guide for more detailed instructions.
1import { useDragSelect } from "@osamaq/drag-select"
useDragSelect(config: Config): DragSelect
Config
1interface Config<ListItem = unknown> { 2 /** 3 * The same array of items rendered on screen in a scrollable view. 4 */ 5 data: Array<ListItem> 6 /** 7 * Key or path to nested key which uniquely identifies an item in the list. 8 * Nested key path is specified using dot notation in a string e.g. `"user.id"`. 9 * 10 * @example 11 * const item = { id: "usr_123", name: "foo" } 12 * useDragSelect({ key: "id" }) 13 */ 14 key: PropertyPaths<ListItem> 15 list: { 16 /** 17 * An [animated ref](https://docs.swmansion.com/react-native-reanimated/docs/core/useAnimatedRef) to 18 * the scrollable view where the items are rendered. 19 * 20 * @example 21 * const animatedRef = useAnimatedRef() 22 * useDragSelect({ list: { animatedRef } }) 23 * return <Animated.FlatList ref={animatedRef} /> 24 */ 25 animatedRef: AnimatedRef<any> 26 /** 27 * Number of columns in the list. 28 * This only matters for vertical lists. 29 * @default 1 30 */ 31 numColumns?: number 32 /** 33 * Number of rows in the list. 34 * This only matters for horizontal lists. 35 * @default 1 36 */ 37 numRows?: number 38 /** 39 * Whether the list is horizontal. 40 * @default false 41 */ 42 horizontal?: boolean 43 /** 44 * Amount of horizontal space between rows. 45 * @default 0 46 */ 47 rowGap?: number 48 /** 49 * Amount of vertical space between columns. 50 * @default 0 51 */ 52 columnGap?: number 53 /** 54 * Height and width of each item in the list. 55 */ 56 itemSize: { 57 width: number 58 height: number 59 } 60 /** 61 * Inner distance between edges of the list and its items. 62 * Use this to account for list headers/footers and/or padding. 63 */ 64 contentInset?: { 65 top?: number 66 bottom?: number 67 left?: number 68 right?: number 69 } 70 } 71 longPressGesture?: { 72 /** 73 * When `true`, long pressing an item will activate selection mode. 74 * @default true 75 */ 76 enabled?: boolean 77 /** 78 * The amount of time in milliseconds an item must be pressed before selection mode activates. 79 * @default 300 80 */ 81 minDurationMs?: number 82 } 83 panGesture?: { 84 /** 85 * When `true`, selection is cleared each time the pan gesture activates. 86 * @default false 87 */ 88 resetSelectionOnStart?: boolean 89 /** 90 * When `true`, panning near the edges will automatically scroll the list. 91 * @default true 92 */ 93 scrollEnabled?: boolean 94 /** 95 * How close should the pointer be to the start of the list before **inverse** scrolling begins. 96 * A value between 0 and 1 where 1 is equal to the height of the list window. 97 * @default 0.15 98 */ 99 scrollStartThreshold?: number 100 /** 101 * How close should the pointer be to the end of the list before scrolling begins. 102 * A value between 0 and 1 where 1 is equal to the height of the list window. 103 * @default 0.85 104 */ 105 scrollEndThreshold?: number 106 /** 107 * The maximum scrolling speed when the pointer is near the starting edge of the list window. 108 * Must be higher than 0. 109 * @default 110 * - 8 on iOS 111 * - 1 on Android 112 */ 113 scrollStartMaxVelocity?: number 114 /** 115 * The maximum scrolling speed when the pointer is at the ending edge of the list window. 116 * Must be higher than 0. 117 * @default 118 * - 8 on iOS 119 * - 1 on Android 120 */ 121 scrollEndMaxVelocity?: number 122 } 123 tapGesture?: { 124 /** 125 * When `true`, tapping an item while selection mode is active will add or remove it from selection. 126 * @default true 127 */ 128 selectOnTapEnabled: boolean 129 } 130 /** 131 * Invoked on the JS thread whenever an item is tapped, but not added to selection. 132 * You may still wrap items with your own pressable component while using this callback to handle the press event. 133 */ 134 onItemPress?: (id: string, index: number) => void 135 /** 136 * Invoked on the JS thread whenever an item is added to selection. 137 */ 138 onItemSelected?: (id: string, index: number) => void 139 /** 140 * Invoked on the JS thread whenever an item is removed from selection. 141 */ 142 onItemDeselected?: (id: string, index: number) => void 143}
DragSelect
1interface DragSelect { 2 /** 3 * Must be used with [`useAnimatedScrollHandler`](https://docs.swmansion.com/react-native-reanimated/docs/scroll/useAnimatedScrollHandler) 4 * and passed to the animated list to use automatic scrolling. 5 * Used to obtain scroll offset and list window size. 6 * 7 * @example 8 * const { onScroll } = useDragSelect() 9 * const scrollHandler = useAnimatedScrollHandler(onScroll) 10 * return <Animated.FlatList onScroll={scrollHandler} /> 11 */ 12 onScroll: (event: ReanimatedScrollEvent) => void 13 gestures: { 14 /** 15 * This returns a [tap](https://docs.swmansion.com/react-native-gesture-handler/docs/gestures/tap-gesture) gesture. 16 * 17 * Do not customize the behavior of this gesture directly. 18 * Instead, [compose](https://docs.swmansion.com/react-native-gesture-handler/docs/gestures/composed-gestures) it with your own. 19 */ 20 createItemPressHandler: (id: string, index: number) => TapGesture 21 /** 22 * This is a single [pan gesture](https://docs.swmansion.com/react-native-gesture-handler/docs/gestures/pan-gesture). 23 * If you need to rely solely on pressing items for selection, you can disable the pan gesture by setting `config.panScrollGesture.enabled` to `false`. See {@link Config.panGesture}. 24 * 25 * Note that the long press gesture can be disabled by setting `config.longPressGesture.enabled` to `false`. See {@link Config.longPressGesture}. 26 * 27 * Do not customize the behavior of this gesture directly. 28 * Instead, [compose](https://docs.swmansion.com/react-native-gesture-handler/docs/gestures/composed-gestures) it with your own. 29 */ 30 panHandler: PanGesture 31 } 32 selection: { 33 /** 34 * Whether the selection mode is active. 35 * Selection mode is active when there are any selected items. 36 * 37 * When active, tapping list items will add them or remove them from selection. 38 * Config callbacks {@link Config.onItemSelected} and {@link Config.onItemDeselected} will be invoked instead of {@link Config.onItemPress}. 39 */ 40 active: DerivedValue<boolean> 41 /** 42 * Add an item to selection. 43 * 44 * Must be invoked on the JS thread. 45 */ 46 add: (id: string) => void 47 /** 48 * Clear all selected items. 49 * Note that this does not trigger {@link Config.onItemDeselected}. 50 * 51 * Must be invoked on the JS thread. 52 */ 53 clear: () => void 54 /** 55 * Remove an item from selection. 56 * 57 * Must be invoked on the JS thread. 58 */ 59 delete: (id: string) => void 60 /** 61 * Indicates whether an item is selected. 62 * 63 * Must be invoked on the JS thread. 64 */ 65 has: (id: string) => boolean 66 /** 67 * Count of currently selected items. 68 */ 69 size: DerivedValue<number> 70 /** 71 * A mapping between selected item IDs and their indices. 72 */ 73 items: DerivedValue<Record<string, number>> 74 /** 75 * Counterpart API for the UI thread. 76 * Note that selection changes are reflected asynchronously on the JS thread and synchronously on the UI thread. 77 */ 78 ui: { 79 /** 80 * Add an item to selection. 81 * 82 * Must be invoked on the UI thread. 83 */ 84 add: (id: string) => void 85 /** 86 * Clear all selected items. 87 * Note that this does not trigger {@link Config.onItemDeselected}. 88 * 89 * Must be invoked on the UI thread. 90 */ 91 clear: () => void 92 /** 93 * Remove an item from selection. 94 * 95 * Must be invoked on the UI thread. 96 */ 97 delete: (id: string) => void 98 /** 99 * Indicates whether an item is selected. 100 * 101 * Must be invoked on the UI thread. 102 */ 103 has: (id: string) => boolean 104 } 105 } 106}
The recipes app contains sample integrations of drag-select.
Sample | Remarks |
---|---|
Code Example of a FlatList integration.Has haptic feedback on selection change. | |
Code Example of a ScrollView integration.List items are animated Pressable components. | |
Code Example of a horizontal list. Only allows selection of a continiuous range. |
Running this utility is not inherently expensive. It works by doing some math on every frame update and only when panning the list. In my testing, I could not manage to get any frame drops at this point. However...
Performance cost comes from the additional logic added in response to changes in selection. You can easily cause frame drops by running expensive animations.
[!TIP] Try to be conservative in list item animations on selection change.
- Certain components and properties are more costly to animate than others
- Don't animate too many things at once
Animations off | Animations on |
---|---|
Running on iPhone 12 mini in dev mode.
This project uses pnpm. You can install it here or through Corepack.
1# install dependencies and start the dev app server 2pnpm install 3pnpm dev start
Consider supporting the following projects:
No vulnerabilities found.
No security vulnerabilities found.