Skip to content
drag and drop

How CMP Elegantly Implements Cross-Application Drag and Drop

In modern desktop applications, drag and drop is an extremely intuitive interaction that greatly improves user experience. It lets users move data and reorganize content by directly manipulating interface elements, dramatically simplifying complex tasks. If you are building a desktop app with Kotlin Compose Multiplatform and want to add this powerful interaction to your app, you have come to the right place!

This article takes a deep dive into implementing drag and drop elegantly in a Compose Multiplatform Desktop application. We start with the core concepts, walk through creating drag sources and drop targets step by step, and use real source code from my project to demonstrate advanced usage and best practices.

Drag and Drop in Compose Multiplatform

Compose Multiplatform provides a set of intuitive modifiers for drag and drop. The core is two modifiers: dragAndDropSource and dragAndDropTarget.

With these two modifiers, your Compose Multiplatform app can:

  • Receive data dragged in from other applications.
  • Let users drag data out of your app.

Let's look at how to create a drag source and a drop target.

Creating a Drag Source

kotlin
fun Modifier.dragAndDropSource(
    drawDragDecoration: DrawScope.() -> Unit,
    transferData: (Offset) -> DragAndDropTransferData?
): Modifier =
    this then
        DragAndDropSourceElement(
            drawDragDecoration = drawDragDecoration,
            // TODO: Expose this as public argument
            detectDragStart = DragAndDropSourceDefaults.DefaultStartDetector,
            transferData = transferData
        )

In your Compose component, use the dragAndDropSource modifier to define a drag source. The modifier takes two parameters:

  • drawDragDecoration: a draw function that renders the dragged component while dragging.
  • transferData: a data-transfer function that provides the data to transfer when the drag starts.

Using my open-source project CrossPaste as an example, here is how to implement an elegant drag source:

Full implementation of SidePastePreviewItemView.kt here

kotlin
// Use a graphicsLayer to record what the drag source renders
val graphicsLayer = rememberGraphicsLayer()

Row(
    modifier =
        Modifier
            .dragAndDropSource(
                drawDragDecoration = {
                    // Draw the drag decoration from the graphicsLayer
                    runBlocking {
                        runCatching {
                            graphicsLayer.toImageBitmap()
                        }.getOrNull()
                    }?.let { bitmap ->
                        drawImage(
                            image = bitmap,
                            topLeft = Offset.Zero,
                            alpha = 0.9f, // Opacity of the dragged content
                        )
                    }
                },
            ) { offset ->
                DragAndDropTransferData(
                    transferable =
                        DragAndDropTransferable(
                            pasteProducer
                                .produce(
                                    pasteData = pasteData,
                                    localOnly = true,
                                    primary = configManager.getCurrentConfig().pastePrimaryTypeOnly,
                                )?.let {
                                    it as DesktopWriteTransferable
                                } ?: DesktopWriteTransferable(LinkedHashMap()),
                        ),
                    supportedActions =
                        listOf(
                            DragAndDropTransferAction.Copy,
                        ),
                    dragDecorationOffset = offset,
                    onTransferCompleted = { action ->
                    },
                )
            }
            ...
) {
    Box(
        modifier =
            Modifier
                .fillMaxSize()
                .drawWithContent {
                    graphicsLayer.record {
                        this@drawWithContent.drawContent()
                    }
                    drawLayer(graphicsLayer)
                },
    ) {
        // Your drag source content goes here
        ...
    }
}

I use rememberGraphicsLayer() to record what the drag source renders, then export it as a bitmap inside dragAndDropSource for drawing. This way, when the user starts dragging, they see a semi-transparent copy of the dragged content that is rendered live and identical to the source. This matters a lot for the interaction: it helps users confirm they grabbed the right thing.

The transferData function must return a DragAndDropTransferData object:

kotlin
@OptIn(ExperimentalComposeUiApi::class)
actual class DragAndDropTransferData(
    /**
     * The object being transferred during a drag-and-drop gesture.
     */
    @property:ExperimentalComposeUiApi
    val transferable: DragAndDropTransferable,

    /**
     * The transfer actions supported by the source of the drag-and-drop session.
     */
    @property:ExperimentalComposeUiApi
    val supportedActions: Iterable<DragAndDropTransferAction>,

    /**
     * The offset of the pointer relative to the drag decoration.
     */
    @property:ExperimentalComposeUiApi
    val dragDecorationOffset: Offset = Offset.Zero,

    /**
     * Invoked when the drag-and-drop gesture completes.
     *
     * The argument to the callback specifies the transfer action with which the gesture completed,
     * or `null` if the gesture did not complete successfully.
     */
    @property:ExperimentalComposeUiApi
    val onTransferCompleted: ((userAction: DragAndDropTransferAction?) -> Unit)? = null,
)
  • DragAndDropTransferable on Desktop is actually AWT's Transferable, which holds the data to transfer. (If you are not familiar with Transferable, think of it as a data container: it reports all the DataFlavors it supports — each DataFlavor is roughly a MIME type — and getTransferData returns the data for a given DataFlavor.)
  • supportedActions is an iterable of DragAndDropTransferAction values describing the supported drag operations (Copy, Move, Link).
  • dragDecorationOffset is the offset of the drag decoration, typically used to adjust where it appears while dragging.
  • onTransferCompleted is a callback invoked when the drag operation completes, useful for handling the result.

Creating a Drop Target

In your Compose component, use the dragAndDropTarget modifier to define a drop target. The modifier takes an implementation of the DragAndDropTarget interface, which contains the methods for handling drag events.

kotlin
interface DragAndDropTarget {

    fun onDrop(event: DragAndDropEvent): Boolean

    fun onStarted(event: DragAndDropEvent) = Unit

    fun onEntered(event: DragAndDropEvent) = Unit

    fun onMoved(event: DragAndDropEvent) = Unit

    fun onExited(event: DragAndDropEvent) = Unit

    fun onChanged(event: DragAndDropEvent) = Unit

    fun onEnded(event: DragAndDropEvent) = Unit
}

In most cases we only care about three methods:

  • onStarted(event: DragAndDropEvent): called when a drag operation starts; useful for updating UI state or preparing the drop target.
  • onDrop(event: DragAndDropEvent): called when the drag is released over the target; handles the actual data transfer.
  • onEnded(event: DragAndDropEvent): called when the drag operation ends; useful for cleaning up state or resetting the UI.

Here is CrossPaste's implementation for accepting dragged data and recording it in the pasteboard:

Full implementation of DragTargetContentView.kt here

kotlin
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun DragTargetContentView() {
    val appWindowManager = koinInject<DesktopAppWindowManager>()
    val copywriter = koinInject<GlobalCopywriter>()
    val pasteConsumer = koinInject<TransferableConsumer>()
    var isDragging by remember { mutableStateOf(false) }
    val animatedAlpha by animateFloatAsState(
        targetValue = if (isDragging) 0.8f else 0f,
        animationSpec = tween(300),
        label = "drag_target_alpha",
    )

    val dragAndDropTarget =
        remember {
            object : DragAndDropTarget {
                override fun onStarted(event: DragAndDropEvent) {
                    isDragging = true
                }

                override fun onEnded(event: DragAndDropEvent) {
                    isDragging = false
                }

                override fun onDrop(event: DragAndDropEvent): Boolean {
                    val transferable = event.awtTransferable

                    val source: String? = appWindowManager.getCurrentActiveAppName()
                    val pasteTransferable = DesktopReadTransferable(transferable)
                    return runBlocking {
                        pasteConsumer.consume(pasteTransferable, source, false)
                    }.isSuccess
                }
            }
        }

    Box(
        modifier =
            Modifier
                .fillMaxSize()
                .dragAndDropTarget(
                    shouldStartDragAndDrop = { true },
                    target = dragAndDropTarget,
                ),
    ) {
        ... // Your UI and animation effects go here
    }
}
  • This implementation tracks the dragging state through onStarted and onEnded. That makes it easy to create an overlay layer above your own UI components and use isDragging to control the visual effect while dragging.

  • In onDrop, on Desktop we can extract awtTransferable from the event — the same data structure the drag source provides. Handle it according to the data types your app cares about; in CrossPaste I iterate over all supported pasteboard types and record all the data so the user can use it later.