Drag Handle¶
By default, dragging can be initiated from anywhere within a draggable item. A drag handle restricts the drag gesture to a specific area within the item, such as a grip icon. This is useful when your item contains interactive elements like buttons or text fields that should not trigger a drag.
Overview¶
Two pieces work together:
- Pass
hasDragHandle = trueto thedraggableItemorreorderableItemmodifier - this stops the item body from initiating drags. - Apply
Modifier.dragHandle(key, state)to the composable that should act as the handle, using the samekeyas the item.
Row(
modifier = Modifier
.reorderableItem(
key = item,
data = item,
state = dndState,
hasDragHandle = true,
onDragEnter = { /* reorder */ },
draggableContent = { ItemRow(item) },
),
) {
Icon(
imageVector = Icons.Rounded.DragHandle,
contentDescription = "Drag handle",
modifier = Modifier.dragHandle(
key = item,
state = dndState,
), // only this area initiates drag
)
Text(item) // the rest of the item stays interactive
}
dragHandle Parameters¶
| Parameter | Type | Default | Description |
|---|---|---|---|
key | Any | Required | The key of the draggable item this handle controls. |
state | DragAndDropState<T> | Required | The drag and drop state. |
enabled | Boolean | true | Whether the drag handle is active. |
dragAfterLongPress | Boolean | false | If true, drag starts after a long press on the handle. |
requireFirstDownUnconsumed | Boolean | false | If true, the first down event on the handle must be unconsumed. |
Full Example: Reorderable List with Handles¶
@Composable
fun DragHandleExample() {
val dndState = rememberDragAndDropState<String>()
var items by remember {
mutableStateOf(listOf("Item 1", "Item 2", "Item 3"))
}
DragAndDropContainer(
state = dndState,
) {
LazyColumn(
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.fillMaxSize().padding(16.dp),
) {
items(items, key = { it }) { item ->
val isDragging = dndState.isDragging(item)
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.graphicsLayer {
alpha = if (isDragging) 0f else 1f
}
.reorderableItem(
key = item,
data = item,
state = dndState,
hasDragHandle = true,
onDragEnter = { state ->
items = items.toMutableList().apply {
val index = indexOf(item)
if (index != -1) {
remove(state.data)
add(index, state.data)
}
}
},
draggableContent = {
HandleRow(text = item)
},
)
.fillMaxWidth()
.height(56.dp)
.padding(horizontal = 16.dp),
) {
// Only this area initiates the drag
Icon(
imageVector = Icons.Rounded.DragHandle,
contentDescription = "Drag handle",
modifier = Modifier.dragHandle(
key = item,
state = dndState,
),
)
Spacer(modifier = Modifier.width(16.dp))
Text(text = item)
}
}
}
}
}
With the Wrapper Composables¶
Inside DraggableItem or ReorderableItem content, the scope provides a Modifier.dragHandle() overload that needs no key or state - and no hasDragHandle flag, since calling it marks the item automatically:
ReorderableItem(state = reorderState, key = item, data = item) {
Row {
Icon(
imageVector = Icons.Rounded.DragHandle,
contentDescription = "Drag handle",
modifier = Modifier.dragHandle(), // scope version
)
Text(item)
}
}
The scope overload accepts the same enabled, dragAfterLongPress, and requireFirstDownUnconsumed parameters.
How It Works¶
When an item has a drag handle:
- Touch events on the item body are not intercepted for drag gestures.
- Only touch events starting inside the handle area will initiate a drag.
- Other interactive elements (buttons, text fields, checkboxes) inside the item remain fully functional.
Tip
Drag handles are especially useful in reorderable lists where each item contains clickable elements. Without a drag handle, tapping a button might accidentally start a drag gesture.