Skip to content

Drag and Drop Overview

Compose DND provides a declarative API for adding drag and drop functionality to your Compose UI. The core building blocks are:

  • DragAndDropState -- Holds the state for all drag and drop operations.
  • DragAndDropContainer -- A container that wraps all draggable items and drop targets.
  • draggableItem modifier -- Makes a composable draggable.
  • dropTarget modifier -- Marks a composable as a drop target.
  • DraggableItem composable -- A wrapper alternative to the draggableItem modifier (see the alternative section).

Creating DragAndDropState

Use rememberDragAndDropState to create and remember a DragAndDropState instance. The type parameter T defines the type of data carried by dragged items.

val dragAndDropState = rememberDragAndDropState<String>()

Parameters

Parameter Type Default Description
dragAfterLongPress Boolean false If true, drag starts after a long press. If false, drag starts on touch move. Applied to all items unless overridden per item.
requireFirstDownUnconsumed Boolean false If true, the first down pointer event must be unconsumed to initiate drag.
reorderHysteresisDistance Dp DefaultReorderHysteresisDistance (8.dp) How far the drag must come back after a reorder swap before that swap is undone. See Reorder List.

DragAndDropContainer

All draggable items and drop targets must be placed inside a DragAndDropContainer. This composable handles pointer input tracking and renders the drag shadow.

DragAndDropContainer(
    state = dragAndDropState,
    modifier = Modifier.fillMaxSize(),
    enabled = true,
) {
    // Draggable items and drop targets go here
}

Parameters

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

Making Items Draggable

Apply the Modifier.draggableItem modifier to any composable. Each item needs a unique key, the data to pass to the drop target when dropped, and a draggableContent that is rendered as the drag shadow:

val isDragging = dragAndDropState.isDragging("item-1")

Text(
    text = "Drag me",
    modifier = Modifier
        .graphicsLayer { alpha = if (isDragging) 0f else 1f }
        .draggableItem(
            key = "item-1",
            data = "Hello World",
            state = dragAndDropState,
            draggableContent = {
                Text("Drag me") // Content shown as the drag shadow
            },
        ),
)

Use DragAndDropState.isDragging(key) to check if a specific item is being dragged - hiding the original with graphicsLayer { alpha = ... } while the shadow follows the pointer is the usual pattern.

The item's configuration (dropTargets, dropStrategy, dragAxis, dropAnimationSpec, draggableContent) is captured when a drag starts and applies for that whole drag, even if the composable recomposes with different values while dragging.

Parameters

Parameter Type Default Description
key Any Required Unique key identifying this item.
data T Required Data passed to the drop target on drop.
state DragAndDropState<T> Required The drag and drop state.
enabled Boolean true Whether this specific item is draggable.
dragAfterLongPress Boolean Inherits from state Override the long-press drag behavior for this item.
requireFirstDownUnconsumed Boolean Inherits from state Override the unconsumed pointer requirement for this item.
dropTargets List<Any> emptyList() Restrict which drop targets this item can be dropped on. Empty means any target.
dropStrategy DropStrategy DropStrategy.Surface Strategy for choosing the hovered drop target.
dragAxis DragAxis DragAxis.Free Constrain drag movement to one axis. See Axis Lock.
hasDragHandle Boolean false If true, drag is initiated from a dragHandle modifier instead of the whole item. See Drag Handle.
dropAnimationSpec AnimationSpec<Offset> SpringSpec() Animation for position when dropping.
onDragStart (DraggedItemState<T>) -> Unit {} Called when a drag of this item starts.
onDragEnd (DraggedItemState<T>) -> Unit {} Called once the drag of this item is over. See Drag Lifecycle.
draggableContent @Composable () -> Unit Required Content rendered as the drag shadow while dragging.

Drag Lifecycle: onDragStart and onDragEnd

draggableItem, reorderableItem, DraggableItem and ReorderableItem all accept onDragStart and onDragEnd callbacks, both (DraggedItemState<T>) -> Unit:

Text(
    text = "Drag me",
    modifier = Modifier.draggableItem(
        key = "item-1",
        data = "Hello",
        state = dragAndDropState,
        onDragStart = { state -> println("Started dragging ${state.data}") },
        onDragEnd = { state -> println("Drag of ${state.data} is over") },
        draggableContent = {
            Text("Drag me")
        },
    ),
)
  • onDragStart fires once, when the drag begins.
  • onDragEnd fires once per drag, after the drag is fully over: for a successful drop, that's after the drop target's onDrop and once the drop animation lands on the target; for a cancelled drag (see cancelDrag) or a release outside any target, it's after the return animation. This makes it a reliable place to persist state -- it runs exactly once per drag, unlike onDragEnter which can fire many times. See Reorder List: Persisting the new order for an example.

Drop Target

Use the Modifier.dropTarget extension to mark any composable as a drop target.

Box(
    modifier = Modifier
        .size(200.dp)
        .dropTarget(
            key = "target-1",
            state = dragAndDropState,
            onDrop = { state ->
                println("Dropped item: ${state.data}")
            },
            onDragEnter = { state ->
                println("Item entered: ${state.data}")
            },
            onDragExit = { state ->
                println("Item exited: ${state.data}")
            },
        )
) {
    Text("Drop here")
}

Parameters

Parameter Type Default Description
key Any Required Unique key identifying this drop target.
state DragAndDropState<T> Required The drag and drop state.
zIndex Float 0f Z-index for overlapping targets. Higher values take priority.
dropAlignment Alignment Alignment.Center Alignment of the dropped item within the target for the drop animation.
dropOffset Offset Offset.Zero Additional offset for the drop animation position.
dropAnimationEnabled Boolean true Whether to animate the drop. If false, the drop callback fires immediately.
canDrop Boolean true Whether this target accepts drops. When false, dragged items cannot hover over or be dropped on this target. Can read state.draggedItem?.data to validate dynamically.
onDrop (DraggedItemState<T>) -> Unit {} Called when an item is dropped on this target. See onDrop timing below.
onDragEnter (DraggedItemState<T>) -> Unit {} Called when a dragged item enters this target.
onDragExit (DraggedItemState<T>) -> Unit {} Called when a dragged item exits this target.

onDrop Timing

onDrop is called once the drop animation has landed the drag shadow on the target, right before the dragged item's onDragEnd. With dropAnimationEnabled = false it is called immediately, as soon as the item is released -- there is no animation to wait for.

If you mutate your list inside onDrop, keep that in mind when the animation is enabled: the visual drop and the state change happen together, which is usually what you want. For a callback that always fires exactly once per drag regardless of where it landed, use the item's onDragEnd instead (see Drag Lifecycle above).

DraggedItemState

The callbacks receive a DraggedItemState<T> with the following properties:

  • key: Any -- The key of the dragged item.
  • data: T -- The data associated with the dragged item.
  • dragAmount: Offset -- The total drag offset from the original position.

Enable/Disable Drag and Drop

Container Level

Disable drag and drop for all items within a container:

DragAndDropContainer(
    state = dragAndDropState,
    enabled = false, // Disables all drag and drop
) {
    // ...
}

Item Level

Disable drag and drop for a specific item by passing enabled = false to the draggableItem modifier:

Text(
    text = "Cannot drag me",
    modifier = Modifier.draggableItem(
        key = "item-1",
        data = "Hello",
        state = dragAndDropState,
        enabled = false, // Only this item is disabled
        draggableContent = { Text("Cannot drag me") },
    ),
)

Observing Drag State

Use DragAndDropState.isDropTargetHovered(key) to check whether a specific drop target is currently hovered by the dragged item:

val isHovered = dragAndDropState.isDropTargetHovered("target-1")

Box(
    modifier = Modifier
        .border(
            width = 2.dp,
            color = if (isHovered) Color.Blue else Color.Gray,
        )
        .dropTarget(
            key = "target-1",
            state = dragAndDropState,
            onDrop = { /* ... */ },
        )
) {
    Text("Drop here")
}

isDropTargetHovered(key) is equivalent to hoveredDropTargetKey == key; hoveredDropTargetKey is still there directly on the state if you need the raw key, for example to look it up in a map.

You can also check if any item is being dragged using DragAndDropState.draggedItem:

val isDragging = dragAndDropState.draggedItem != null

Cancelling a Drag Programmatically

DragAndDropState.cancelDrag() aborts the drag in progress, if any: the drag shadow animates back to the dragged item, no drop target receives onDrop, the hovered target (if any) gets its onDragExit, and the item's onDragEnd fires once the return animation ends. It does nothing when no drag is in progress or the item was already released.

This is useful to abort a drag on a back press, an Escape key, or when the dragged data becomes invalid mid-drag:

BackHandler(enabled = dragAndDropState.draggedItem != null) {
    dragAndDropState.cancelDrag()
}

Disabling a DragAndDropContainer (enabled = false) while a drag is in progress also calls cancelDrag() internally, so the drag never gets stuck. A gesture canceled by the system is canceled the same way.

Full Working Example

@Composable
fun DragAndDropExample() {
    val dragAndDropState = rememberDragAndDropState<Int>()
    var isDropped by remember { mutableStateOf(false) }

    DragAndDropContainer(
        state = dragAndDropState,
        modifier = Modifier.fillMaxSize(),
    ) {
        Column(
            verticalArrangement = Arrangement.spacedBy(20.dp),
            modifier = Modifier.fillMaxSize().padding(20.dp),
        ) {
            // Source area
            Box(
                contentAlignment = Alignment.Center,
                modifier = Modifier
                    .fillMaxWidth()
                    .weight(1f)
                    .border(1.dp, Color.Gray, RoundedCornerShape(16.dp)),
            ) {
                if (!isDropped) {
                    val isDragging = dragAndDropState.isDragging(1)

                    RedBox(
                        text = "Drag me",
                        modifier = Modifier
                            .graphicsLayer {
                                alpha = if (isDragging) 0f else 1f
                            }
                            .draggableItem(
                                key = 1,
                                data = 1,
                                state = dragAndDropState,
                                draggableContent = {
                                    RedBox(text = "Drag me")
                                },
                            ),
                    )
                }
            }

            // Target area
            val isHovered = dragAndDropState.isDropTargetHovered("target")
            Box(
                contentAlignment = Alignment.Center,
                modifier = Modifier
                    .fillMaxWidth()
                    .weight(1f)
                    .border(
                        width = 2.dp,
                        color = if (isHovered) Color.Blue else Color.Gray,
                        shape = RoundedCornerShape(16.dp),
                    )
                    .dropTarget(
                        key = "target",
                        state = dragAndDropState,
                        onDrop = { isDropped = true },
                    ),
            ) {
                if (isDropped) {
                    RedBox(text = "Dropped!")
                } else {
                    Text("Drop here", color = Color.Gray)
                }
            }
        }
    }
}

@Composable
private fun RedBox(
    text: String,
    modifier: Modifier = Modifier,
) {
    Box(
        modifier = modifier
            .size(100.dp)
            .background(Color.Red, RoundedCornerShape(16.dp)),
        contentAlignment = Alignment.Center,
    ) {
        Text(text, color = Color.White)
    }
}

Alternative: DraggableItem Wrapper Composable

If you prefer wrapping composables over modifier chains, the DraggableItem composable offers the same features:

DraggableItem(
    state = dragAndDropState,
    key = "item-1",
    data = "Hello World",
) {
    // isDragging is available in this scope
    Text(
        text = "Drag me",
        modifier = Modifier
            .graphicsLayer {
                alpha = if (isDragging) 0f else 1f
            }
    )
}

It accepts the same parameters as the draggableItem modifier, with two differences:

  • draggableContent is optional -- when null, the item content itself is used as the drag shadow.
  • The content lambda runs in a DraggableItemScope that provides key, isDragging, and a scope-based Modifier.dragHandle() that doesn't need key/state arguments.

The same applies to reorder: ReorderableItem is the wrapper alternative to the reorderableItem modifier -- see Reorder List.