Sortable
A vertical list the user reorders by dragging — by a handle, or by holding the row.
Installation
Sortable ships with the library — install it once, then import:
import { Sortable } from "@delacour/react-native-ui/sortable";Or copy the source into your project, to own and edit it:
bunx delacour@latest add sortableUsage
import { Item } from "@/components/ui/item";
import { Sortable } from "@/components/ui/sortable";
import { useState } from "react";
import { View } from "react-native";
const TRACKS: Record<string, string> = {
dawn: "Dawn Chorus",
tide: "Low Tide",
ember: "Ember",
};
export default function Example() {
const [order, setOrder] = useState(["dawn", "tide", "ember"]);
return (
<View className="flex-1 justify-center p-4">
<Sortable value={order} onReorder={setOrder} gap={8}>
{order.map((id) => (
<Sortable.Item key={id} id={id}>
<Item variant="outline">
<Item.Content>
<Item.Title>{TRACKS[id]}</Item.Title>
</Item.Content>
<Item.Actions>
<Sortable.Handle />
</Item.Actions>
</Item>
</Sortable.Item>
))}
</Sortable>
</View>
);
}The order is yours: render the rows from value and store what onReorder hands back. A drag
renders nothing — the rows move by transform on the UI thread — and onReorder fires once, after
the row has landed, with the new order and { id, from, to }.
Long press
activation="long-press" lifts the whole row after a hold (longPressDelay, 220ms by default). A
move before the hold completes still scrolls the screen. The handle stays as a visual hint.
<Sortable activation="long-press" value={order} onReorder={setOrder}>
…
</Sortable>Variable heights
Every row measures itself, so rows can be any height. A row changes slot as soon as its edge crosses the middle of the next row — not when it is nearer that slot — so a short row passes a tall one without feeling stuck.
Pinned and disabled rows
<Sortable.Item id="inbox" isPinned>…</Sortable.Item>
<Sortable.Item id="later" isDisabled>…</Sortable.Item>A pinned row never moves and the others reorder around it. A disabled row cannot be lifted, but
the others move past it. isDisabled on the root disables the whole list.
In a list group
Wrap each ListGroup.Item in a Sortable.Item and the lifted row takes an opaque card surface and
a shadow, so the row it passes never shows through.
Auto-scroll
Inside a scroll view, pass its animated ref as scrollRef. A row dragged within
autoscrollThreshold (72pt) of either edge scrolls the list, up to autoscrollSpeed (8pt per
frame) right at the edge.
import Animated, { useAnimatedRef } from "react-native-reanimated";
const scrollRef = useAnimatedRef<Animated.ScrollView>();
<Animated.ScrollView ref={scrollRef}>
<Sortable value={order} onReorder={setOrder} scrollRef={scrollRef}>
…
</Sortable>
</Animated.ScrollView>Every row stays mounted — there is no virtualisation. Sortable is comfortable to about a hundred
rows. It reads isPinned from the Sortable.Items that are its direct children (or a .map()); a
row behind a wrapper component counts as unpinned.
Haptics
A lift and each slot passed play a haptic — { lift: "medium", pass: "selection" } by default.
<Sortable haptic={{ lift: "heavy", pass: "light" }} …>…</Sortable>
<Sortable haptic={false} …>…</Sortable>Custom handle
useSortableItem() reads the enclosing row: its id, index, isActive (a shared value, for an
animated style that follows the lift without a render), and moveUp / moveDown.
Accessibility
- A drag is invisible to a screen reader, so the handle — or the row, in
long-pressmode — offers Move up and Move down actions and the value "Position 2 of 5". An action reorders immediately and announces the new position. - A row only offers the moves it can make: none when pinned or disabled, no Move up at the top.
- The handle is a 44-point button labelled "Drag to reorder"; pass
accessibilityLabelto change it. - With reduce motion on, rows snap to their slots instead of springing. The row under your finger still follows it.
API Reference
Anatomy
| Part | What it is |
|---|---|
Sortable | The list — owns the drag, the order it renders and auto-scroll |
Sortable.Item | One row: measured, lifted and moved. Keyed by id |
Sortable.Handle | The grip a row is dragged by, and its screen-reader actions |
Sortable
Extends ViewProps.
Prop
Type
Sortable.Item
Extends ViewProps.
Prop
Type
Sortable.Handle
Extends ViewProps. Children replace the grip glyph.
Prop
Type
Exports
import {
Sortable,
type SortableProps,
type SortableReorderDetails,
reorderItems,
useSortableItem,
sortableVariants,
} from "@delacour/react-native-ui/sortable";