Skip to content

Conditional Drop

By default, any dragged item can be dropped on any drop target. Conditional drop lets you control which drop targets accept which items, enabling scenarios such as typed drop zones, capacity limits, or permission-based dropping.

Using canDrop

The canDrop parameter on the dropTarget modifier is a plain Boolean, evaluated by Compose like any other modifier argument. Because it is just a boolean expression, it can read live state -- including state.draggedItem?.data -- to accept or reject whichever item is currently being dragged:

Box(
    modifier = Modifier
        .dropTarget(
            key = "numbers-only",
            state = dragAndDropState,
            // Only accept items whose data is a number
            canDrop = dragAndDropState.draggedItem?.data is Int,
            onDrop = { state ->
                println("Accepted: ${state.data}")
            },
        )
) {
    Text("Numbers only")
}

When canDrop is false, the target is excluded from hover detection entirely: it will not trigger onDragEnter, onDragExit, or onDrop for that item, and hoveredDropTargetKey never points to it.

Usage Scenarios

Typed Drop Zones

Accept only specific data types in different zones:

@Composable
fun TypedDropZonesExample() {
    val dragAndDropState = rememberDragAndDropState<Any>()
    val draggedData = dragAndDropState.draggedItem?.data

    DragAndDropContainer(state = dragAndDropState) {
        Row(
            horizontalArrangement = Arrangement.spacedBy(16.dp),
            modifier = Modifier.fillMaxWidth(),
        ) {
            // Only accepts strings
            Box(
                modifier = Modifier
                    .weight(1f)
                    .height(200.dp)
                    .dropTarget(
                        key = "text-zone",
                        state = dragAndDropState,
                        canDrop = draggedData is String,
                        onDrop = { /* handle text drop */ },
                    )
            ) {
                Text("Text Zone")
            }

            // Only accepts integers
            Box(
                modifier = Modifier
                    .weight(1f)
                    .height(200.dp)
                    .dropTarget(
                        key = "number-zone",
                        state = dragAndDropState,
                        canDrop = draggedData is Int,
                        onDrop = { /* handle number drop */ },
                    )
            ) {
                Text("Number Zone")
            }
        }
    }
}

Capacity Limits

Prevent dropping when a zone is full:

val maxItems = 5
var droppedItems by remember { mutableStateOf(listOf<String>()) }

Box(
    modifier = Modifier
        .dropTarget(
            key = "limited-zone",
            state = dragAndDropState,
            canDrop = droppedItems.size < maxItems,
            onDrop = { state ->
                droppedItems = droppedItems + state.data
            },
        )
) {
    Text("${droppedItems.size} / $maxItems")
}

Preventing Self-Drop

Prevent an item from being dropped back onto itself:

Box(
    modifier = Modifier
        .dropTarget(
            key = item.id,
            state = dragAndDropState,
            canDrop = dragAndDropState.draggedItem?.key != item.id,
            onDrop = { state ->
                // Handle drop from a different item
            },
        )
)

Interaction with dropTargets Parameter

The canDrop parameter on the drop target is different from the dropTargets parameter on the draggableItem modifier:

Feature dropTargets (on draggableItem) canDrop (on dropTarget)
Defined on The dragged item The drop target
Filters by Drop target keys (allowlist) Custom boolean expression on live state
Use case "This item can only go to zones A, B" "This zone only accepts certain items"

Both can be combined. The dragged item first checks its dropTargets list, and then the drop target checks its canDrop value.

Visual Feedback

Combine canDrop with isDropTargetHovered to provide visual feedback:

@Composable
fun ConditionalDropWithFeedback() {
    val dragAndDropState = rememberDragAndDropState<String>()
    val isHovered = dragAndDropState.isDropTargetHovered("target-1")
    val draggedText = dragAndDropState.draggedItem?.data

    Box(
        modifier = Modifier
            .border(
                width = 2.dp,
                color = if (isHovered) Color.Green else Color.Gray,
            )
            .dropTarget(
                key = "target-1",
                state = dragAndDropState,
                // Only accept short strings
                canDrop = draggedText == null || draggedText.length <= 10,
                onDrop = { state ->
                    println("Dropped: ${state.data}")
                },
            )
    ) {
        Text("Short strings only")
    }
}

Note

When canDrop is false, the drop target will not trigger onDragEnter, onDragExit, or onDrop for that item. hoveredDropTargetKey will not be set to this target's key, so visual hover feedback will not activate either.

Tip

reorderableItem and ReorderableItem have their own canDrop parameter for keeping specific rows out of a reorder -- for example pinned or locked items. See Reorder List: Pinned or Locked Items.