---
title: Cookbook | Drag & Drop
contributors:
 - byte-barista
---

import CodeSandbox, {CodeFile} from '../../../../components/code-sandbox/index.tsx';


# Drag & Drop

Building drag-and-drop functionality is a pretty common task in web development. With Qwik, you can easily implement drag-and-drop functionality by using the `onDragStart$`, `onDragOver$`, `onDragLeave$`, and `onDrop$` APIs. You need to have in mind that Qwik processes events asynchronously. This means that some APIs such as `event.preventDefault()`, `e.dataTransfer.getData()` or `e.dataTransfer.setData()` do not work as expected.

To work around this limitations, Qwik provides a [sync$()](/docs/cookbook/sync-events) API which allows you to process events synchronously. For preventing the default behavior, 
you can use the `preventdefault:dragover` and `preventdefault:drop` attributes.


### Basic example

<CodeSandbox src="/src/routes/demo/cookbook/drag&drop/basic" style={{ height: '30em' }}>
</CodeSandbox>


<CodeFile src="/src/routes/demo/cookbook/drag&drop/basic">
```tsx
import { component$, useSignal, sync$, $ } from '@builder.io/qwik';

export default component$(() => {
  const items1 = useSignal([
    { id: 1, content: '📱 Phone' },
    { id: 2, content: '💻 Laptop' },
    { id: 3, content: '🎧 Headphones' },
  ]);

  const items2 = useSignal([
    { id: 4, content: '⌚️ Watch' },
    { id: 5, content: '🖱 Mouse' },
    { id: 6, content: '⌨️ Keyboard' },
  ]);

  return (
    <div class="flex min-h-screen justify-center gap-8 bg-gray-50 p-8">
      <div
        class="h-[25em] w-80 rounded-xl border-2 border-dashed border-gray-300 bg-white p-6 shadow-sm transition-all duration-300 hover:border-gray-400 [&[data-over]]:border-blue-300 [&[data-over]]:bg-blue-50"
        preventdefault:dragover
        preventdefault:drop
        onDragOver$={sync$((_: DragEvent, currentTarget: HTMLDivElement) => {
          currentTarget.setAttribute('data-over', 'true');
        })}
        onDragLeave$={sync$((_: DragEvent, currentTarget: HTMLDivElement) => {
          currentTarget.removeAttribute('data-over');
        })}
        onDrop$={[
          sync$((e: DragEvent, currentTarget: HTMLDivElement) => {
            const id = e.dataTransfer?.getData('text');
            currentTarget.dataset.droppedId = id;
            currentTarget.removeAttribute('data-over');
          }),
          $((_, currentTarget) => {
            const id = currentTarget.dataset.droppedId;
            if (id) {
              const itemId = parseInt(id);
              const item = [...items2.value].find((i) => i.id === itemId);
              if (item) {
                items2.value = items2.value.filter((i) => i.id !== itemId);
                items1.value = [...items1.value, item];
              }
            }
          }),
        ]}
      >
        <h3 class="mb-4 text-lg font-semibold text-gray-700">Container 1</h3>
        {items1.value.map((item) => (
          <div
            key={item.id}
            data-id={item.id}
            class="min-h-[62px] mb-3 cursor-move select-none rounded-lg border border-gray-200 bg-white p-4 transition-all duration-200 hover:-translate-y-1 hover:shadow-md active:scale-95"
            draggable
            onDragStart$={sync$(
              (e: DragEvent, currentTarget: HTMLDivElement) => {
                const itemId = currentTarget.getAttribute('data-id');
                if (e.dataTransfer && itemId) {
                  e.dataTransfer?.setData('text/plain', itemId);
                }
              }
            )}
          >
            <span class="text-lg text-gray-700">{item.content}</span>
          </div>
        ))}
      </div>

      <div
        class="h-[25em] w-80 rounded-xl border-2 border-dashed border-gray-300 bg-white p-6 shadow-sm transition-all duration-300 hover:border-gray-400 [&[data-over]]:border-blue-300 [&[data-over]]:bg-blue-50"
        preventdefault:dragover
        preventdefault:drop
        onDragOver$={sync$((_: DragEvent, currentTarget: HTMLDivElement) => {
          currentTarget.setAttribute('data-over', 'true');
        })}
        onDragLeave$={sync$((_: DragEvent, currentTarget: HTMLDivElement) => {
          currentTarget.removeAttribute('data-over');
        })}
        onDrop$={[
          sync$((e: DragEvent, currentTarget: HTMLDivElement) => {
            const id = e.dataTransfer?.getData('text');
            currentTarget.dataset.droppedId = id;
            currentTarget.removeAttribute('data-over');
          }),
          $((_, currentTarget) => {
            const id = currentTarget.dataset.droppedId;
            if (id) {
              const itemId = parseInt(id);
              const item = [...items1.value].find((i) => i.id === itemId);
              if (item) {
                items1.value = items1.value.filter((i) => i.id !== itemId);
                items2.value = [...items2.value, item];
              }
            }
          }),
        ]}
      >
        <h3 class="mb-4 text-lg font-semibold text-gray-700">Container 2</h3>
        {items2.value.map((item) => (
          <div
            key={item.id}
            data-id={item.id}
            class="min-h-[62px] mb-3 cursor-move select-none rounded-lg border border-gray-200 bg-white p-4 transition-all duration-200 hover:-translate-y-1 hover:shadow-md active:scale-95"
            draggable
            onDragStart$={sync$(
              (e: DragEvent, currentTarget: HTMLDivElement) => {
                const itemId = currentTarget.getAttribute('data-id');
                if (e.dataTransfer && itemId) {
                  e.dataTransfer?.setData('text/plain', itemId);
                }
              }
            )}
          >
            <span class="text-lg text-gray-700">{item.content}</span>
          </div>
        ))}
      </div>
    </div>
  );
});
```
</CodeFile>


### Advanced example with sorting


<CodeSandbox src="/src/routes/demo/cookbook/drag&drop/advanced" style={{ height: '30em' }}>
</CodeSandbox>


<CodeFile src="/src/routes/demo/cookbook/drag&drop/advanced">
```tsx
import { component$, sync$, useSignal, $ } from '@builder.io/qwik';

type Item = {
  id: number;
  content: string;
};

export default component$(() => {
  const items1 = useSignal<Item[]>([
    { id: 1, content: '📱 Phone' },
    { id: 2, content: '💻 Laptop' },
    { id: 3, content: '🎧 Headphones' },
  ]);

  const items2 = useSignal<Item[]>([
    { id: 4, content: '⌚️ Watch' },
    { id: 5, content: '🖱 Mouse' },
    { id: 6, content: '⌨️ Keyboard' },
  ]);

  return (
    <div class="flex min-h-screen justify-center gap-8 bg-gray-50 p-8">
      <div
        data-dropzone
        class="h-[25em] w-80 rounded-xl border-2 border-dashed border-gray-300 bg-white p-6 shadow-sm transition-all duration-300 hover:border-gray-400 [&[data-over]]:border-blue-300 [&[data-over]]:bg-blue-50"
        preventdefault:dragover
        preventdefault:drop
        onDragOver$={sync$((_: DragEvent, currentTarget: HTMLDivElement) => {
          currentTarget.setAttribute('data-over', 'true');
        })}
        onDragLeave$={sync$((_: DragEvent, currentTarget: HTMLDivElement) => {
          currentTarget.removeAttribute('data-over');
        })}
        onDrop$={[
          sync$((e: DragEvent, currentTarget: HTMLDivElement) => {
            const id = e.dataTransfer?.getData('text/plain');
            currentTarget.dataset.droppedId = id;
            currentTarget.removeAttribute('data-over');
          }),
          $((e, currentTarget) => {
            const draggedElementId = currentTarget.dataset.droppedId;
            const isDropZone = currentTarget.hasAttribute('data-dropzone');

            if (draggedElementId) {
              const itemId = parseInt(draggedElementId);
              const item = items2.value.find((i) => i.id === itemId);

              if (item && isDropZone) {
                items2.value = items2.value.filter((i) => i.id !== itemId);
                items1.value = [...items1.value, item];
              } else {
                const newItems = [...items1.value];
                const targetId = parseInt(
                  (e.target as HTMLDivElement).dataset.id || '0'
                );
                if (targetId === 0) return;

                const targetIndex = items1.value.findIndex(
                  (i) => i.id === targetId
                );
                const draggedIndex = items1.value.findIndex(
                  (i) => i.id === itemId
                );

                if (draggedIndex !== -1) {
                  // Sorting in the same container
                  swapElements(newItems, draggedIndex, targetIndex);
                  items1.value = newItems;
                } else {
                  // Sorting between containers
                  if (!item) return;
                  items2.value = items2.value.filter((i) => i.id !== itemId);
                  insertElement(newItems, targetIndex, item);
                  items1.value = newItems;
                }
              }
            }
          }),
        ]}
      >
        <h3 class="mb-4 text-lg font-semibold text-gray-700">Container 1</h3>
        {items1.value.map((item) => (
          <div
            key={item.id}
            data-id={item.id}
            class="min-h-[62px] mb-3 cursor-move select-none rounded-lg border border-gray-200 bg-white p-4 transition-all duration-200 hover:-translate-y-1 hover:shadow-md active:scale-95"
            draggable
            onDragStart$={sync$(
              (e: DragEvent, currentTarget: HTMLDivElement) => {
                const itemId = currentTarget.getAttribute('data-id');
                if (e.dataTransfer && itemId) {
                  e.dataTransfer.setData('text/plain', itemId);
                }
              }
            )}
          >
            <span class="text-lg text-gray-700">{item.content}</span>
          </div>
        ))}
      </div>

      <div
        class="h-[25em] w-80 rounded-xl border-2 border-dashed border-gray-300 bg-white p-6 shadow-sm transition-all duration-300 hover:border-gray-400 [&[data-over]]:border-blue-300 [&[data-over]]:bg-blue-50"
        data-dropzone
        preventdefault:dragover
        preventdefault:drop
        onDragOver$={(_: DragEvent, currentTarget: HTMLDivElement) => {
          currentTarget.setAttribute('data-over', 'true');
        }}
        onDragLeave$={[
          sync$((_: DragEvent, currentTarget: HTMLDivElement) => {
            currentTarget.removeAttribute('data-over');
          }),
        ]}
        onDrop$={[
          sync$((e: DragEvent, currentTarget: HTMLDivElement) => {
            const id = e.dataTransfer?.getData('text/plain');
            currentTarget.dataset.droppedId = id;
            currentTarget.removeAttribute('data-over');
          }),
          $((e, currentTarget) => {
            const draggedElementId = currentTarget.dataset.droppedId;
            const isDropZone = currentTarget.hasAttribute('data-dropzone');

            if (draggedElementId) {
              const itemId = parseInt(draggedElementId);
              const item = items1.value.find((i) => i.id === itemId);

              if (isDropZone && item) {
                items1.value = items1.value.filter((i) => i.id !== itemId);
                items2.value = [...items2.value, item];
              } else {
                const targetId = parseInt(
                  (e.target as HTMLDivElement).dataset.id || '0'
                );
                if (targetId === 0) return;
                const newItems = [...items2.value];
                const draggedIndex = items2.value.findIndex(
                  (i) => i.id === itemId
                );
                const targetIndex = items2.value.findIndex(
                  (i) => i.id === targetId
                );
                if (draggedIndex !== -1) {
                  // Sorting in the same container
                  swapElements(newItems, targetIndex, draggedIndex);
                  items2.value = newItems;
                } else {
                  // Sorting between containers
                  if (!item) return;
                  items1.value = items1.value.filter((i) => i.id !== itemId);
                  insertElement(newItems, targetIndex, item);
                  items2.value = newItems;
                }
              }
            }
          }),
        ]}
      >
        <h3 class="mb-4 text-lg font-semibold text-gray-700">Container 2</h3>
        {items2.value.map((item) => (
          <div
            key={item.id}
            data-id={item.id}
            class="min-h-[62px] mb-3 cursor-move select-none rounded-lg border border-gray-200 bg-white p-4 transition-all duration-200 hover:-translate-y-1 hover:shadow-md active:scale-95"
            draggable
            onDragStart$={sync$(
              (e: DragEvent, currentTarget: HTMLDivElement) => {
                const itemId = currentTarget.getAttribute('data-id');
                if (e.dataTransfer && itemId) {
                  e.dataTransfer.setData('text/plain', itemId);
                }
              }
            )}
          >
            <span class="text-lg text-gray-700">{item.content}</span>
          </div>
        ))}
      </div>
    </div>
  );
});

function swapElements(arr: Item[], index1: number, index2: number) {
  arr[index1] = arr.splice(index2, 1, arr[index1])[0];

  return arr;
}

function insertElement(arr: Item[], index: number, item: Item) {
  arr.splice(index, 0, item);
  return arr;
}

```
</CodeFile>




You can find more information about drag-and-drop in the [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API).