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$() 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
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>
);
});
Advanced example with sorting
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;
}
You can find more information about drag-and-drop in the MDN documentation.