# Reorderable list

> A drag-to-sort list: a long press (or the grip) lifts a row with a little scale and a shadow, the other rows slide out of its way with a tick per swap, the list scrolls itself near the edges, and the row settles into its slot on release.

- Type: Component (data)
- Access: Free, open source (MIT)
- Page: https://goodui.dev/native/components/reorderable-list
- Markdown: https://goodui.dev/native/components/reorderable-list.md
- Registry item: https://goodui.dev/native/r/reorderable-list.json
- Dependencies: react-native-gesture-handler, react-native-reanimated, react-native-svg
- Keywords: react native draggable list, drag and drop sort, sortable list, reorder items, drag handle, long press drag, playlist order, expo reorder

## When to use

- The person sets an order that matters: playlist, workout, checklist steps, payment priority, home widgets.
- An edit mode for a list that is normally read-only (pass disabled outside edit mode with dragHandle).
- Short to medium lists, up to about a hundred rows, of equal height.

## When not to use

- Rows of different heights or thousands of rows: this list is not virtualized and needs a fixed itemHeight.
- Sorting by a value (date, price): give a sort control such as segmented-control instead of manual dragging.
- Rows with swipe actions: use swipe-actions; the two gestures compete.
- Moving items between groups or a grid: build a dedicated board.

## Installation

With the shadcn CLI (needs a components.json in the Expo app), then `npx expo install --fix` so native dependencies match your Expo SDK. The foundation comes along the first time.

```bash
npx shadcn@latest add https://goodui.dev/native/r/reorderable-list.json
npx expo install --fix
```

The component lands in `components/goodui-native/reorderable-list/` and imports the foundation from `components/goodui-native/foundation/` with relative paths; it also installs locale.

Built-in strings follow `<GooduiLocaleProvider>` (one [language pack](https://goodui.dev/native/r/locale-tr.json) translates the whole kit); the `labels` prop overrides them for one instance.

## Usage

```tsx
import { useState } from "react";
import { Text, View } from "react-native";
import { ReorderableList } from "@/components/goodui-native/reorderable-list/reorderable-list";

type Song = { id: string; title: string; artist: string };

export function Queue({ initial }: { initial: Song[] }) {
  const [songs, setSongs] = useState(initial);
  return (
    <ReorderableList
      data={songs}
      keyExtractor={(s) => s.id}
      getItemLabel={(s) => s.title}
      accessibilityLabel="Up next"
      onReorder={setSongs}
      itemHeight={60}
      style={{ flex: 1 }}
      contentContainerStyle={{ padding: 16 }}
      renderItem={({ item }) => (
        <View style={{ paddingHorizontal: 16 }}>
          <Text>{item.title}</Text>
          <Text>{item.artist}</Text>
        </View>
      )}
    />
  );
}
```

## Examples

### Grip handle with an edit mode and row taps

```tsx
<ReorderableList
  data={tasks}
  keyExtractor={(t) => t.id}
  dragHandle
  disabled={!editing}
  onReorder={setTasks}
  onItemPress={(t) => toggleDone(t.id)}
  renderItem={({ item }) => <TaskRow task={item} />}
/>
```

### Know where it moved

```tsx
<ReorderableList
  data={cards}
  keyExtractor={(c) => c.id}
  onReorder={(next, { from, to }) => {
    setCards(next);
    api.saveOrder(next.map((c) => c.id));
  }}
  renderItem={({ item, active }) => <CardRow card={item} lifted={active} />}
/>
```

## API reference

### ReorderableList

`import { ReorderableList } from "@/components/goodui-native/reorderable-list/reorderable-list";`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `data` (required) | `T[]` | – | Items in their current order. Update it from `onReorder`. |
| `keyExtractor` (required) | `(item: T, index: number) => string` | – | Stable, unique key per item. |
| `renderItem` (required) | `(info: { item: T; index: number; active: boolean }) => ReactNode` | – | Renders one row's content, `itemHeight` tall. `active` is true while the row is lifted. Do not put Pressables inside: use `onItemPress`, which works together with dragging on every platform. |
| `onReorder` (required) | `(data: T[], move: { from: number; to: number }) => void` | – | Called after a drop or an accessibility move with the items in their new order. |
| `itemHeight` | `number` | `60` | Height of every row in points. Rows are fixed height so they can slide without measuring. Default 60. |
| `dragHandle` | `boolean` | `false` | `false` (default): long-press a row to lift it (on the web, press and drag with the mouse). `true`: a grip on the trailing edge drags at once and the rest of the row stays free for taps. The grip hides while `disabled`, so it doubles as an edit mode. |
| `delayLongPress` | `number` | `300` | Hold time in milliseconds before a row lifts. Default 300. |
| `onItemPress` | `(item: T, index: number) => void` | – | Tap on a row. |
| `variant` | `"inset" \| "plain"` | `"inset"` | `inset` puts the rows on a rounded surface with hairlines; `plain` draws no surface (style rows yourself). |
| `autoScroll` | `boolean` | `true` | Scroll the list when a lifted row nears its top or bottom edge. Needs a bounded height. Default `true`. |
| `haptic` | `boolean` | `true` | `false` silences the lift tap and the swap ticks. |
| `disabled` | `boolean` | `false` | Turns dragging off, for example outside an edit mode. |
| `getItemLabel` | `(item: T, index: number) => string` | – | Name of an item for screen readers and move announcements, usually its title. |
| `accessibilityLabel` | `string` | – | Name of the list, for example "Playlist". |
| `labels` | `Partial<ReorderableListLabels>` | – | Overrides for the built-in strings; a GooduiLocaleProvider pack sets them app-wide. |
| `style` | `StyleProp<ViewStyle>` | – | Style of the scrolling viewport. Give it a height or `flex: 1` for auto-scroll. |
| `contentContainerStyle` | `StyleProp<ViewStyle>` | – | Padding around the rows inside the scroll view. |
| `testID` | `string` | – | Identifier for end-to-end tests. |

## Accessibility

- Rows are list items in a list, each with Move up and Move down accessibility actions (plus activate when onItemPress is set). Every move is announced as '<label>, position 3 of 8'.
- On the web every row is focusable: Alt + Arrow Up and Alt + Arrow Down move it (declared with aria-keyshortcuts), Enter presses it.
- getItemLabel names rows on native and in announcements; on the web the visible text names the row.
- The grip is hidden from screen readers because the actions do the same; it has a 44 pt target.

## Motion

- Lift: the row scales to 1.03 on the snappy spring and a raised surface with a shadow fades in under it.
- The lifted row follows the finger 1:1 with a little give past the first and last slot; the others slide to their new slots on the smooth spring as it crosses their midpoints.
- Auto-scroll: within 64 pt of the top or bottom edge the list scrolls up to 14 pt per frame, faster closer to the edge, and the row keeps tracking the finger.
- Drop: the row settles into its slot on the smooth spring while the shadow fades.
- Reduced motion: no lift scale, the other rows jump to their slots, the drop and the shadow use a short fade.

## Haptics

- drag-start when a row lifts, drop when it lands
- select on every swap while dragging and on each accessibility move

## Performance

- Dragging, sliding and auto-scroll run on the UI thread with one shared positions map; React state changes only on lift and drop.
- Rows are absolutely positioned at a fixed height and not virtualized: keep lists to roughly a hundred rows.
- The auto-scroll frame callback runs only while a row is lifted.

## Notes for AI

- keyExtractor must return stable unique keys; onReorder gives the new array, store it (setData). If you ignore it the list keeps the dragged order on screen.
- All rows are itemHeight tall (default 60). Content taller than that is clipped by the slot, so size renderItem to it.
- Do not put Pressable, Switch or buttons in renderItem: they swallow the drag on the web. Use onItemPress for row taps.
- Give the list a bounded height (style={{ flex: 1 }} or a fixed height) when it can be longer than the screen, or auto-scroll has nothing to scroll.
- variant='inset' draws a rounded surface; with variant='plain' put your own row styling in renderItem (it sits on the screen background).
- On the web without dragHandle a row drags as soon as the mouse moves, so touch scrolling over rows on mobile web is not available; use dragHandle for touch-heavy web.

## Related

- [List row](https://goodui.dev/native/components/list-row.md): Settings and navigation cells with a token-colored icon tile, title, subtitle and a trailing value, switch, badge, check or chevron, grouped in inset sections with headers and footers. On touch down the highlight floods in, the tile sinks and the chevron leans forward; on release the highlight lingers and fades.
- [Swipe actions](https://goodui.dev/native/components/swipe-actions.md): Row actions that unfold with the finger; a long swipe lets the edge action take over and run, and destructive actions collapse the row before it leaves.
- [Sticky header list](https://goodui.dev/native/components/sticky-header-list.md): A sectioned list whose sticky headers change as they stick: the title shrinks toward the leading edge and a surface with a hairline (frosted on the web) fades in under it. A contacts-style index on the side jumps between sections while a large letter follows the finger, with a tick per letter.

## Guidance for AI tools

Reorderable list: A drag-to-sort list: a long press (or the grip) lifts a row with a little scale and a shadow, the other rows slide out of its way with a tick per swap, the list scrolls itself near the edges, and the row settles into its slot on release. Use only the documented props. Keep accessibility, reduced motion and both color schemes intact when adapting it.

Full library index: https://goodui.dev/native/llms.txt
