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-xs 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-xs 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-xs 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-xs 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.