Skip to content

Reorder List

Compose DND provides a higher-level API for reordering items in a list. The reorder API builds on top of the core drag and drop primitives, combining both draggable and drop target behavior into a single ReorderableItem composable.

Creating ReorderState

Use rememberReorderState to create and remember a ReorderState instance:

val reorderState = rememberReorderState<String>()

Parameters

Parameter Type Default Description
dragAfterLongPress Boolean false If true, drag starts after a long press. Applied to all items unless overridden.

ReorderContainer

All reorderable items must be placed inside a ReorderContainer:

ReorderContainer(
    state = reorderState,
    modifier = Modifier.fillMaxSize(),
) {
    // Reorderable items go here
}

Parameters

Parameter Type Default Description
state ReorderState<T> Required The reorder state.
modifier Modifier Modifier Modifier for the container.
enabled Boolean true Whether reordering is enabled.
content @Composable () -> Unit Required The content of the container.

ReorderableItem

ReorderableItem is both a draggable item and a drop target. Use it for each item that can be reordered:

ReorderableItem(
    state = reorderState,
    key = item.id,
    data = item,
    onDragEnter = { state ->
        // Reorder the list when a dragged item enters this item's area
    },
) {
    Text(
        text = item.name,
        modifier = Modifier
            .graphicsLayer {
                alpha = if (isDragging) 0f else 1f
            }
    )
}

Parameters

Parameter Type Default Description
state ReorderState<T> Required The reorder state.
key Any Required Unique key identifying this item.
data T Required Data associated with this item.
modifier Modifier Modifier Modifier for the item.
zIndex Float 0f Z-index for overlapping items.
enabled Boolean true Whether this item is reorderable.
dragAfterLongPress Boolean Inherits from state Override long-press drag behavior.
requireFirstDownUnconsumed Boolean Inherits from state Override unconsumed pointer requirement.
dropTargets List<Any> emptyList() Restrict which targets this item can be dropped on.
dropStrategy DropStrategy DropStrategy.SurfacePercentage Strategy for choosing the hovered target.
onDrop (DraggedItemState<T>) -> Unit {} Called when an item is dropped on this target.
onDragEnter (DraggedItemState<T>) -> Unit {} Called when a dragged item enters this target.
onDragExit (DraggedItemState<T>) -> Unit {} Called when a dragged item exits this target.
dropAnimationSpec AnimationSpec<Offset> SpringSpec() Animation for position when dropping.
sizeDropAnimationSpec AnimationSpec<Size> SpringSpec() Animation for size when dropping.
draggableContent (@Composable () -> Unit)? null Custom drag shadow content.
content @Composable ReorderableItemScope.() -> Unit Required The item content.

ReorderableItemScope

ReorderableItemScope extends DraggableItemScope and provides:

  • key: Any -- The key of this item.
  • isDragging: Boolean -- Whether this item is currently being dragged.

Reorder Logic with onDragEnter

The reorder logic is implemented in the onDragEnter callback. When a dragged item enters another item's area, you update the list order:

onDragEnter = { state ->
    items = items.toMutableList().apply {
        val targetIndex = indexOf(item)
        if (targetIndex == -1) return@ReorderableItem
        remove(state.data)
        add(targetIndex, state.data)
    }
}

Note

The onDragEnter callback receives the DraggedItemState of the item being dragged. Use state.data to identify the dragged item and reposition it in your list.

Observing Reorder State

You can observe the current drag state through ReorderState:

// Currently dragged item
val draggedItem = reorderState.draggedItem

// Key of the drop target currently being hovered
val hoveredKey = reorderState.hoveredDropTargetKey

Full Working Example with LazyColumn

@Composable
fun ReorderListExample() {
    val reorderState = rememberReorderState<String>()
    var items by remember {
        mutableStateOf(
            listOf("Item 1", "Item 2", "Item 3", "Item 4", "Item 5")
        )
    }
    val lazyListState = rememberLazyListState()

    ReorderContainer(
        state = reorderState,
        modifier = Modifier.fillMaxSize().padding(20.dp),
    ) {
        LazyColumn(
            state = lazyListState,
            verticalArrangement = Arrangement.spacedBy(12.dp),
            modifier = Modifier.fillMaxSize(),
        ) {
            items(items, key = { it }) { item ->
                ReorderableItem(
                    state = reorderState,
                    key = item,
                    data = item,
                    onDrop = {},
                    onDragEnter = { state ->
                        items = items.toMutableList().apply {
                            val index = indexOf(item)
                            if (index == -1) return@ReorderableItem
                            remove(state.data)
                            add(index, state.data)
                        }
                    },
                    draggableContent = {
                        ItemCard(
                            text = item,
                            isDragShadow = true,
                        )
                    },
                    modifier = Modifier.fillMaxWidth(),
                ) {
                    ItemCard(
                        text = item,
                        modifier = Modifier
                            .graphicsLayer {
                                alpha = if (isDragging) 0f else 1f
                            }
                            .fillMaxWidth(),
                    )
                }
            }
        }
    }
}

@Composable
private fun ItemCard(
    text: String,
    isDragShadow: Boolean = false,
    modifier: Modifier = Modifier,
) {
    Card(
        modifier = modifier.height(60.dp),
        elevation = CardDefaults.cardElevation(
            defaultElevation = if (isDragShadow) 8.dp else 2.dp,
        ),
    ) {
        Box(
            contentAlignment = Alignment.CenterStart,
            modifier = Modifier
                .fillMaxSize()
                .padding(horizontal = 16.dp),
        ) {
            Text(text = text)
        }
    }
}

Tip

Use graphicsLayer { alpha = if (isDragging) 0f else 1f } to hide the original item while it is being dragged, so only the drag shadow is visible.

reorderableItem Modifier

As an alternative to the ReorderableItem composable wrapper, you can use the Modifier.reorderableItem modifier. This combines draggableItem and dropTarget into a single modifier and reduces boilerplate.

val dndState = rememberDragAndDropState<String>()

DragAndDropContainer(state = dndState) {
    LazyColumn(
        verticalArrangement = Arrangement.spacedBy(12.dp),
    ) {
        items(items, key = { it }) { item ->
            val isDragging = dndState.isDragging(item)

            ItemCard(
                text = item,
                modifier = Modifier
                    .graphicsLayer { alpha = if (isDragging) 0f else 1f }
                    .reorderableItem(
                        key = item,
                        data = item,
                        state = dndState,
                        onDragEnter = { state ->
                            items = items.toMutableList().apply {
                                val index = indexOf(item)
                                if (index != -1) {
                                    remove(state.data)
                                    add(index, state.data)
                                }
                            }
                        },
                        draggableContent = {
                            ItemCard(text = item, isDragShadow = true)
                        },
                    )
                    .fillMaxWidth(),
            )
        }
    }
}

Note

When using the modifier API, use rememberDragAndDropState and DragAndDropContainer directly instead of rememberReorderState and ReorderContainer. Use DragAndDropState.isDragging(key) to check drag state.