Drag and Drop com JavaScript
Aprenda drag-and-drop em JavaScript de duas formas: eventos de mouse e a API nativa HTML5 com draggable, dragover, drop e DataTransfer, com exemplo prático.
Neste tutorial, exploraremos a funcionalidade de drag-and-drop em JavaScript, um recurso poderoso que aumenta a interatividade das páginas web. Existem duas formas de implementá-la: a abordagem com eventos de mouse (mousedown/mousemove/mouseup), que oferece controle total sobre o movimento, e a API nativa de Drag and Drop do HTML5 (draggable + os eventos drag*), que já está integrada ao navegador. Vamos abordar ambas, ver onde cada uma se encaixa e, em seguida, construir um exemplo prático em que um ícone de lâmpada ilumina uma área escura quando você o arrasta para dentro dela.
O que é Drag-and-Drop?
Drag-and-drop é uma interação de interface do usuário que permite aos usuários pegar um objeto e movê-lo para um local diferente na tela. Você a encontra em todo lugar: arrastar arquivos no sistema operacional, reorganizar itens em um jogo, fazer upload de fotos soltando-as em uma página ou reordenar uma lista de tarefas. O padrão sempre tem três papéis:
- Um draggable — o elemento que o usuário pega.
- Um drop target (ou droppable) — a área que pode recebê-lo.
- Um payload — dados opcionais transportados da origem ao destino (por exemplo, o id do item sendo movido).
Escolhendo uma Abordagem
Ambas as técnicas são válidas; escolha com base no que você precisa.
- Eventos de mouse (
mousedown,mousemove,mouseup) — você rastreia manualmente o ponteiro e move o elemento por conta própria. Use isto quando o elemento precisar seguir o cursor pixel a pixel (controles deslizantes, telas livres, ferramentas de desenho, física personalizada). Também é o que você estende para telas de toque comtouchstart/touchmove/touchend. - Drag and Drop nativo do HTML5 (
draggable="true"+dragstart/dragover/drop) — o navegador cuida da imagem "fantasma" e do gesto de arrastar por você, e fornece um objetoDataTransferpara transportar dados. Use isto para transferir algo entre zonas (uploads de arquivos, reordenação de listas, soltar itens em um carrinho). Não funciona por padrão em dispositivos de toque.
Vamos demonstrar a abordagem com eventos de mouse no exemplo principal e, em seguida, resumir a API nativa.
Conceitos Fundamentais de Drag-and-Drop em JavaScript
O Algoritmo de Drag'n'Drop
- Iniciar o Arraste:
- O processo começa quando o usuário clica no elemento e mantém o botão do mouse pressionado.
- Arrastar o Elemento:
- À medida que o mouse se move, o elemento segue o caminho do cursor pela tela.
- Soltar o Elemento:
- O elemento é liberado quando o usuário solta o botão do mouse, posicionando o elemento em uma nova localização.
Entendendo os Droppables
Droppables são áreas designadas para receber os elementos arrastáveis. Essas áreas detectam quando um objeto arrastável está sobre elas e podem disparar ações específicas como resposta.
Garanta que sua funcionalidade de drag-and-drop seja compatível com toque. Usuários de dispositivos móveis devem conseguir arrastar e soltar com gestos de toque. Considere implementar eventos de toque (touchstart, touchmove, touchend) ou usar uma biblioteca leve para compatibilidade entre dispositivos.
Exemplo Interativo: Área Clara e Escura
Vamos colocar a teoria em prática com um exemplo simples, mas interativo. Usaremos um ícone de lâmpada como objeto arrastável. Quando esse ícone for movido sobre uma área escura, a área se iluminará, simulando o efeito de uma luz sendo acesa.
Configurando o HTML e o CSS
Primeiro, definimos a estrutura básica e o estilo. Incluímos uma caixa escura e um ícone de lâmpada.
Estrutura HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Interactive Lighting with Drag and Drop</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.1/css/all.min.css" />
<style>
#darkArea {
width: 300px;
height: 300px;
background-color: #333;
position: relative;
margin-top: 20px;
}
#lightIcon {
font-size: 48px;
color: #ccc;
cursor: pointer;
position: absolute;
}
</style>
</head>
<body>
<div id="main">
<div id="darkArea"></div>
<i id="lightIcon" class="fas fa-lightbulb"></i>
</div>
<script>
// JavaScript will be added here.
</script>
</body>
</html>Implementando o JavaScript
Agora, vamos adicionar funcionalidade para tornar a lâmpada arrastável e reativa à área escura.
Explicação do Código JavaScript
<script>
// Get references to the light bulb icon and the dark area on the webpage
var lightIcon = document.getElementById("lightIcon");
var darkArea = document.getElementById("darkArea");
// Variables to track whether the dragging is active and to store position data
var active = false;
var initialX, initialY, currentX, currentY, xOffset = 0, yOffset = 0;
// Listen for the mouse down event on the light bulb icon
lightIcon.addEventListener("mousedown", function(e) {
// Record the starting position of the mouse and adjust by any existing offset
initialX = e.clientX - xOffset;
initialY = e.clientY - yOffset;
// Set the active flag to true, indicating that dragging has started
active = true;
});
// Listen for mouse movement across the entire document
document.addEventListener("mousemove", function(e) {
// If not dragging, don't do anything
if (!active) return;
// Calculate the new position of the mouse
currentX = e.clientX - initialX;
currentY = e.clientY - initialY;
// Update the offset with the new position
xOffset = currentX;
yOffset = currentY;
// Move the light bulb icon to the new position
lightIcon.style.transform = "translate3d(" + currentX + "px, " + currentY + "px, 0)";
});
// Listen for the mouse up event across the entire document
document.addEventListener("mouseup", function() {
// Save the final position of the light bulb
initialX = currentX;
initialY = currentY;
// Set the active flag to false, indicating dragging has ended
active = false;
// Check if the light bulb is inside the dark area
if (isInside(darkArea, lightIcon)) {
// Change the background color of the dark area to yellow
darkArea.style.backgroundColor = "yellow";
// Change the color of the light bulb to yellow
lightIcon.style.color = "yellow";
} else {
// Revert the dark area's color to dark
darkArea.style.backgroundColor = "#333";
// Revert the light bulb's color to gray
lightIcon.style.color = "#ccc";
}
});
// Function to check if the light bulb is inside the dark area
function isInside(container, element) {
// Get the position of the container and the element
var containerRect = container.getBoundingClientRect();
var elementRect = element.getBoundingClientRect();
// Return true if the element is within the container's boundaries
return (
elementRect.left >= containerRect.left &&
elementRect.right <= containerRect.right &&
elementRect.top >= containerRect.top &&
elementRect.bottom <= containerRect.bottom
);
}
</script>Este script torna a lâmpada arrastável com o mouse e reage quando você a solta sobre a área escura. Veja o que cada parte faz:
- Configuração: O script obtém referências ao ícone da lâmpada e à área escura, e declara as variáveis usadas para rastrear o arraste (
active, a posição inicial do mouse, o deslocamento atual). - Iniciar o arraste (
mousedown): Quando você pressiona o botão do mouse sobre a lâmpada, o script registra a posição inicial do cursor (menos qualquer deslocamento de um arraste anterior) e defineactive = true. O deslocamento é importante — sem ele, o ícone voltaria à origem a cada novo arraste. - Mover (
mousemove): Enquantoactivefortrue, o script calcula o quanto o cursor se deslocou e aplica isso como uma transformaçãotranslate3d, fazendo o ícone seguir o ponteiro. Seactiveforfalse, o handler retorna imediatamente sem fazer nada. - Soltar (
mouseup): Quando você solta o botão, o script armazena a posição final, defineactive = falsee chamaisInsidepara decidir se a lâmpada pousou na área escura. Se sim, tanto a área quanto a lâmpada ficam amarelas; caso contrário, voltam às cores padrão. - Teste de colisão (
isInside): Esse auxiliar compara os retângulos delimitadores dos dois elementos comgetBoundingClientRect()e retornatruesomente quando a lâmpada está totalmente contida dentro das bordas da área escura.
Use translate3d em suas transformações CSS para arrastar elementos. Ele utiliza aceleração por GPU, resultando em movimentos mais suaves e menor carga na CPU, o que é essencial para aplicações com alto desempenho.
Exemplo Completo
Agora é hora de ver tudo em ação:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Interactive Lighting with Drag and Drop</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.1/css/all.min.css" />
<style>
#darkArea {
width: 300px;
height: 300px;
background-color: #333;
position: relative;
margin-top: 20px;
}
#lightIcon {
font-size: 48px;
color: #ccc;
cursor: pointer;
position: absolute;
}
</style>
</head>
<body>
<div id="main">
<p>Move the light into the dark area to light it up!</p>
<div id="darkArea"></div>
<i id="lightIcon" class="fas fa-lightbulb"></i>
</div>
<script>
// Get references to the light bulb icon and the dark area on the webpage
var lightIcon = document.getElementById("lightIcon");
var darkArea = document.getElementById("darkArea");
// Variables to track whether the dragging is active and to store position data
var active = false;
var initialX,
initialY,
currentX,
currentY,
xOffset = 0,
yOffset = 0;
// Listen for the mouse down event on the light bulb icon
lightIcon.addEventListener("mousedown", function (e) {
// Record the starting position of the mouse and adjust by any existing offset
initialX = e.clientX - xOffset;
initialY = e.clientY - yOffset;
// Set the active flag to true, indicating that dragging has started
active = true;
});
// Listen for mouse movement across the entire document
document.addEventListener("mousemove", function (e) {
// If not dragging, don't do anything
if (!active) return;
// Calculate the new position of the mouse
currentX = e.clientX - initialX;
currentY = e.clientY - initialY;
// Update the offset with the new position
xOffset = currentX;
yOffset = currentY;
// Move the light bulb icon to the new position
lightIcon.style.transform = "translate3d(" + currentX + "px, " + currentY + "px, 0)";
});
// Listen for the mouse up event across the entire document
document.addEventListener("mouseup", function () {
// Save the final position of the light bulb
initialX = currentX;
initialY = currentY;
// Set the active flag to false, indicating dragging has ended
active = false;
// Check if the light bulb is inside the dark area
if (isInside(darkArea, lightIcon)) {
// Change the background color of the dark area to yellow
darkArea.style.backgroundColor = "yellow";
lightIcon.style.color = "yellow";
} else {
// Revert the dark area's color to dark
darkArea.style.backgroundColor = "#333";
// Revert the light bulb's color to gray
lightIcon.style.color = "#ccc";
}
});
// Function to check if the light bulb is inside the dark area
function isInside(container, element) {
// Get the position of the container and the element
var containerRect = container.getBoundingClientRect();
var elementRect = element.getBoundingClientRect();
// Return true if the element is within the container's boundaries
return elementRect.left >= containerRect.left && elementRect.right <= containerRect.right && elementRect.top >= containerRect.top && elementRect.bottom <= containerRect.bottom;
}
</script>
</body>
</html>Principais Eventos de Mouse Utilizados:
mousedown: Este evento é disparado quando o usuário pressiona o botão do mouse sobre o ícone da lâmpada. Marca o início do arraste e registra a posição inicial do cursor.mousemove: Este evento é disparado quando o mouse se move. Se o arraste estiver ativo (ou seja, o botão do mouse ainda estiver pressionado), calcula a nova posição do ícone com base no movimento do cursor e atualiza a posição da lâmpada na tela.mouseup: Este evento ocorre quando o usuário solta o botão do mouse, marcando o fim do arraste. Verifica se a lâmpada está dentro dos limites da área escura para decidir se deve alterar a cor de fundo da área.
Como aprendemos no artigo sobre Eventos de Mouse, esses eventos são fundamentais para criar funcionalidades interativas de drag-and-drop, permitindo que elementos de uma página web sejam movidos dinamicamente e interajam com o mouse. (Se você quiser posicionar o elemento com precisão, o capítulo sobre Coordenadas em JavaScript explica a diferença entre clientX/clientY, pageX/pageY e getBoundingClientRect().)
A API Nativa de Drag and Drop do HTML5
A técnica com eventos de mouse é ótima quando você precisa que um elemento siga o cursor com exatidão. Mas quando seu objetivo real é transferir algo — soltar um cartão em uma coluna, um item em um carrinho, um arquivo em uma zona de upload — a API de Drag and Drop integrada ao navegador exige menos código e gerencia o gesto de arraste por você.
Tornando um Elemento Arrastável
Qualquer elemento se torna arrastável ao definir o atributo draggable como true. Links e imagens são arrastáveis por padrão; tudo o mais não é.
<div id="item" draggable="true">Drag me</div>
<div id="dropzone">Drop here</div>Os Eventos de Drag and Drop
A API dispara uma sequência de eventos. Os mais importantes são:
| Evento | Disparado em | Quando |
|---|---|---|
dragstart | o elemento arrastado | no momento em que o arraste começa |
dragover | o drop target | continuamente enquanto um arrastável está sobre ele |
drop | o drop target | quando o usuário solta sobre ele |
dragend | o elemento arrastado | quando o arraste termina (solto ou cancelado) |
Transportando Dados com DataTransfer
Cada evento de arraste expõe event.dataTransfer, um objeto usado para anexar um payload em dragstart e lê-lo novamente em drop.
const item = document.getElementById("item");
const zone = document.getElementById("dropzone");
// 1. Attach a payload when the drag starts.
item.addEventListener("dragstart", (e) => {
e.dataTransfer.setData("text/plain", item.id);
});
// 2. By default elements are NOT valid drop targets.
// Prevent the default to allow a drop.
zone.addEventListener("dragover", (e) => {
e.preventDefault();
});
// 3. Read the payload and move the element on drop.
zone.addEventListener("drop", (e) => {
e.preventDefault();
const id = e.dataTransfer.getData("text/plain");
zone.appendChild(document.getElementById(id));
});O erro mais comum com a API nativa é esquecer o e.preventDefault() dentro do handler de dragover. Sem ele, o navegador rejeita o elemento como drop target e o evento drop nunca é disparado. Os dados definidos com setData só ficam disponíveis novamente quando drop é executado — eles não podem ser lidos durante dragover por razões de segurança.
O par setData(format, value) / getData(format) permite transportar texto simples, URLs (text/uri-list), HTML ou suas próprias chaves de string personalizadas. Para uploads de arquivos, leia e.dataTransfer.files, que é um FileList assim como um <input type="file">.
Eventos de Mouse vs. API Nativa em Resumo
- Opte por eventos de mouse quando o movimento precisar ser livre e pixel-preciso, ou quando você precisar de suporte a toque.
- Opte pela API nativa quando estiver movendo um item discreto de um lugar para outro e quiser que o navegador gerencie a imagem de arraste e o gesto.
Para aprofundar-se no sistema de eventos subjacente, consulte Introdução aos Eventos do Navegador, e para interações acessíveis lembre-se que o drag-and-drop deve sempre ter uma alternativa via teclado — consulte Considerações de Acessibilidade do DOM.
Conclusão
O drag-and-drop torna as interfaces mais diretas e intuitivas. Agora você tem duas ferramentas para isso: a abordagem com eventos de mouse, que move um elemento pixel a pixel sob controle manual total, e a API nativa de Drag and Drop do HTML5, que deixa o navegador gerenciar o gesto enquanto você transporta dados pelo DataTransfer. Escolha eventos de mouse para movimento livre e suporte a toque, e a API nativa para transferir itens entre zonas. Qualquer que seja sua escolha, forneça uma alternativa acessível via teclado para que todos os usuários possam concluir a mesma tarefa.