Drag & Drop Physics
Drag-and-drop là một trong những interaction phức tạp nhất để làm đúng về mặt animation — có ít nhất 5 state cần xử lý riêng biệt: pickup, dragging, hover over target, drop, và cancel. Mỗi state cần visual feedback khác nhau.
- Motion principles Theory
- Spring physics Research
- Stagger animation Pattern
- Gesture feedback Pattern
- Page transitions Pattern
Kéo icon ⠿ để sắp xếp lại thứ tự
Năm state của drag
idle → [pickup] → dragging → [hover target] → [drop/cancel]
↕
[leave target]
idle: trạng thái bình thường.
pickup: người dùng bắt đầu drag — element “nhấc lên”, tạo shadow, scale nhẹ.
dragging: element follow con trỏ/ngón tay.
hover target: di chuyển qua drop zone — target highlight.
drop: thả xuống — element snap về vị trí cuối.
cancel: escape hoặc drop vào vùng không hợp lệ — element spring về vị trí ban đầu.
Pickup animation
.dragging {
transform: scale(1.04) rotate(1.5deg);
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.2);
cursor: grabbing;
z-index: 1000;
transition: transform 0.15s cubic-bezier(0.2, 0, 0, 1),
box-shadow 0.15s ease-out;
}
Scale lên nhẹ + rotate nhỏ + shadow mạnh hơn = element “nhấc lên khỏi mặt phẳng”.
Rotate (1–2 độ) là chi tiết quan trọng: vật thật bị nhấc lên thường nghiêng nhẹ do không cân bằng.
Placeholder
Khi element được drag ra khỏi vị trí, placeholder giữ khoảng trống — tránh layout collapse.
function startDrag(el) {
const placeholder = el.cloneNode(false);
placeholder.style.opacity = '0';
placeholder.style.pointerEvents = 'none';
el.parentNode.insertBefore(placeholder, el.nextSibling);
return placeholder;
}
Placeholder có cùng kích thước với element gốc nhưng invisible. Khi element drag qua các vị trí khác, placeholder di chuyển để preview vị trí drop.
Placeholder movement (sort animation)
Khi drag qua một item trong list, item đó slide ra nhường chỗ cho placeholder.
.list-item {
transition: transform 0.2s cubic-bezier(0.2, 0, 0, 1);
}
.list-item.displaced-down { transform: translateY(80px); }
.list-item.displaced-up { transform: translateY(-80px); }
80px là ví dụ — thực tế dùng chiều cao của item bị drag.
function updateDisplacements(dragIndex, overIndex) {
items.forEach((item, i) => {
item.classList.remove('displaced-up', 'displaced-down');
if (dragIndex < overIndex && i > dragIndex && i <= overIndex) {
item.classList.add('displaced-up');
} else if (dragIndex > overIndex && i >= overIndex && i < dragIndex) {
item.classList.add('displaced-down');
}
});
}
Drop animation
Khi thả, element cần animate về vị trí cuối cùng — không teleport.
function dropElement(el, targetRect) {
const currentRect = el.getBoundingClientRect();
const dx = targetRect.left - currentRect.left;
const dy = targetRect.top - currentRect.top;
// FLIP technique: animate từ current position về target
el.style.transition = 'transform 0.3s cubic-bezier(0.2, 0, 0, 1)';
el.style.transform = `translate(${dx}px, ${dy}px)`;
el.addEventListener('transitionend', () => {
el.style.transition = '';
el.style.transform = '';
// Insert el vào đúng vị trí trong DOM
targetContainer.insertBefore(el, targetPosition);
}, { once: true });
}
Kỹ thuật FLIP (First, Last, Invert, Play): đo vị trí trước và sau, animate sự chênh lệch.
Cancel — spring về vị trí cũ
Nếu drop không hợp lệ:
function cancelDrag(el, originRect) {
const currentRect = el.getBoundingClientRect();
const dx = originRect.left - currentRect.left;
const dy = originRect.top - currentRect.top;
// Spring về với overshoot nhẹ
el.style.transition = 'transform 0.4s cubic-bezier(0.34, 1.2, 0.64, 1)';
el.style.transform = `translate(${dx}px, ${dy}px) rotate(0deg) scale(1)`;
el.addEventListener('transitionend', () => {
el.style.transition = '';
el.style.transform = '';
el.classList.remove('dragging');
}, { once: true });
}
cubic-bezier(0.34, 1.2, 0.64, 1) tạo overshoot nhỏ — element “bật nhẹ” khi về chỗ cũ, cảm giác như bị từ chối và nhảy trở lại.
Touch vs pointer events
Dùng Pointer Events API thay vì mouse hoặc touch events riêng — xử lý cả hai.
el.addEventListener('pointerdown', startDrag);
document.addEventListener('pointermove', onDrag);
document.addEventListener('pointerup', endDrag);
// Quan trọng: capture pointer để tiếp tục nhận event kể cả khi di chuyển ra ngoài element
el.setPointerCapture(event.pointerId);
setPointerCapture đảm bảo element tiếp tục nhận pointermove dù ngón tay đi xa — không bị mất track.
Tham khảo: Spring Parameters, Gesture Feedback Motion, Ease vs Spring.