HTML5 Drag and Drop Breaks in Whale Browser. Here's the Fix.
The Bug Report That Made Me Groan
I built a Kanban board. Three columns: To Do, In Progress, Done. Drag cards between columns. Simple enough. I used the HTML5 Drag and Drop API. Clean code, worked perfectly.
Chrome? Perfect. Firefox? Perfect. Safari? Perfect.
Then QA filed a bug report.
"Kanban drag doesn't work. When I grab a card, it just selects text."
Which browser? Naver Whale.
My first instinct was to ignore it. Whale is a niche browser, right? Wrong. In South Korea, Whale has significant market share. Especially in B2B. Plenty of companies use Whale internally. I couldn't just hand-wave it away.
Here's what the code looked like—textbook HTML5 DnD:
function KanbanCard({ task, onMoveTask }) {
const handleDragStart = (e: React.DragEvent) => {
e.dataTransfer.setData('text/plain', task.id);
e.dataTransfer.effectAllowed = 'move';
};
return (
<div
draggable
onDragStart={handleDragStart}
className="kanban-card"
>
<h3>{task.title}</h3>
<p>{task.description}</p>
</div>
);
}
function KanbanColumn({ status, tasks, onMoveTask }) {
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
};
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
const taskId = e.dataTransfer.getData('text/plain');
onMoveTask(taskId, status);
};
return (
<div
onDragOver={handleDragOver}
onDrop={handleDrop}
className="kanban-column"
>
<h2>{status}</h2>
{tasks.map(task => (
<KanbanCard key={task.id} task={task} onMoveTask={onMoveTask} />
))}
</div>
);
}
draggable attribute. onDragStart sets data. onDragOver calls preventDefault(). onDrop receives data. Nothing wrong with this pattern in any textbook.
Except in Whale, grabbing a card and dragging produced... nothing. Text selection or zero response. Either way, Kanban was dead.
Browser Gesture Hijacking
I opened DevTools and traced the events. dragstart fired. Good. But dragover never came. drop never came. The event chain started and then vanished into thin air.
At first I suspected Whale's Chromium version was outdated. Whale is Chromium-based, so HTML5 DnD should be fully supported. It wasn't a version issue.
I tried CSS fixes: user-select: none, -webkit-user-drag: element, various pointer-events values. None of it helped.
After hours of debugging, I found the root cause.
Whale's Built-in Gesture System
Naver Whale ships with mouse gesture features out of the box. Drag the mouse and you can go back, go forward, open a new tab. It also has a sidebar open gesture and drag-to-search (select text by dragging to auto-search).
These features hijack native drag events. Whale's gesture system operates at a higher priority than the web page's drag event handlers. When a user grabs a card and drags, Whale intercepts the event before dragover and drop can fire on the page. The browser says, "Oh, a drag? Let me open the sidebar for you!" while your Kanban board sits there doing nothing.
Think of it like an overzealous security guard. You're trying to deliver a package to the 3rd floor office. The delivery driver enters the building (dragstart fires). But the guard at the lobby intercepts: "A package? I'll handle this! Looks like it should go to the sidebar!" The 3rd floor (onDrop) never gets its delivery.
Telling users to "go to Whale settings and disable mouse gestures" is a terrible solution. It doesn't scale. Users don't want to change their browser settings for your app. I needed a fundamental fix.
Why dnd-kit Works Everywhere
The real insight was that this isn't a Whale-specific bug. It's a structural limitation of the HTML5 Drag and Drop API.
The HTML5 DnD Problem
HTML5 DnD was standardized from an API that originated in Internet Explorer 5. Early 2000s design. Before smartphones. Before touch events existed.
The critical flaw: HTML5 DnD events go through the browser's native drag system. When a user clicks and drags, the browser first interprets the action as a "drag," then forwards events to the web page. Any browser with its own drag-related features can intercept events before they reach your code.
Pointer Events: The Bypass
Pointer Events (pointerdown, pointermove, pointerup) are low-level input events that unify mouse, touch, and pen input. They don't go through the browser's drag interpretation layer.
HTML5 DnD event flow:
User input → Browser's native drag system → (can be hijacked!) → Web page
Pointer Events flow:
User input → Web page (direct)
HTML5 DnD is like a landline phone routed through a switchboard operator. The operator can intercept your call. Pointer Events are a direct line. No middleman, no interception.
dnd-kit's Sensor Architecture
dnd-kit is a React drag-and-drop library that doesn't use HTML5 DnD at all. It uses a sensor-based architecture built on Pointer Events.
| Sensor | Input Method | Purpose |
|---|
PointerSensor | Pointer Events | Mouse/touch/pen (unified) |
KeyboardSensor | Keyboard events | Accessibility |
TouchSensor | Touch Events | Touch-only (legacy) |
MouseSensor | Mouse Events | Mouse-only (legacy) |
The default PointerSensor bypasses the browser's gesture system entirely. Whale can hijack native drag events all day long—dnd-kit doesn't care because it never uses them.
Here's the same Kanban board rewritten with dnd-kit:
import {
DndContext,
closestCorners,
PointerSensor,
KeyboardSensor,
useSensor,
useSensors,
DragEndEvent,
} from '@dnd-kit/core';
import {
SortableContext,
useSortable,
verticalListSortingStrategy,
} from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
function KanbanBoard({ columns, onMoveTask }) {
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
distance: 8, // 8px movement before drag starts
},
}),
useSensor(KeyboardSensor) // Accessibility: drag with keyboard
);
const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event;
if (!over) return;
onMoveTask(active.id, over.id);
};
return (
<DndContext
sensors={sensors}
collisionDetection={closestCorners}
onDragEnd={handleDragEnd}
>
<div className="kanban-board">
{columns.map(column => (
<KanbanColumn key={column.id} column={column} />
))}
</div>
</DndContext>
);
}
function SortableCard({ task }) {
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({ id: task.id });
const style = {
transform: CSS.Transform.toString(transform),
transition,
opacity: isDragging ? 0.5 : 1,
};
return (
<div
ref={setNodeRef}
style={style}
{...attributes}
{...listeners}
className="kanban-card"
>
<h3>{task.title}</h3>
<p>{task.description}</p>
</div>
);
}
More code than the HTML5 version? Yes. But look at what you get:
| Feature | HTML5 DnD | dnd-kit |
|---|
| Whale browser | ❌ Broken | ✅ Works |
| Touch devices | ❌ Not supported | ✅ Built-in |
| Keyboard a11y | ❌ Build it yourself | ✅ KeyboardSensor |
| Drag preview | Limited ghost image | Full customization |
| Animation | Build it yourself | CSS transform, automatic |
| Sortable lists | Build it yourself | @dnd-kit/sortable |
Practical Migration Tips
Switching from HTML5 DnD to dnd-kit isn't just swapping imports. There are a few gotchas I ran into during migration.
Activation Constraints: Click vs. Drag
Without activationConstraint, even a click triggers a drag. If your cards have onClick handlers or links inside them, they'll break.
useSensor(PointerSensor, {
activationConstraint: {
distance: 8, // Must move 8px before drag starts
},
})
I've found 5-10px works well. Too large and dragging feels sluggish. Too small and clicks accidentally trigger drags.
Touch Devices: Long Press Pattern
On mobile, you need to distinguish between scrolling and dragging. Use delay + tolerance:
useSensor(PointerSensor, {
activationConstraint: {
delay: 250, // Hold for 250ms to start drag
tolerance: 5, // Allow 5px finger wobble while holding
},
})
This creates a "long press" activation pattern. Normal touches and scrolls pass through unaffected. The tolerance prevents accidental cancellation from finger tremor.
DragOverlay: Custom Drag Previews
One of my biggest frustrations with HTML5 DnD was the ghost image. Semi-transparent, barely customizable. dnd-kit's DragOverlay lets you render any React component as the drag preview:
<DndContext onDragStart={handleDragStart} onDragEnd={handleDragEnd}>
{/* columns */}
<DragOverlay>
{activeTask ? (
<div className="drag-preview">
<h3>{activeTask.title}</h3>
<span className="badge">Moving</span>
</div>
) : null}
</DragOverlay>
</DndContext>
Any React component. Change icons, resize, show completely different UI while dragging. Miles ahead of setDragImage().
CSS Transform Performance
dnd-kit moves elements using CSS transform during drag, not layout properties like top/left. This matters for performance. Layout property changes trigger reflow across the entire page. CSS transforms use GPU-accelerated compositing. At 60fps during a drag operation, that difference is night and day.
const style = {
transform: CSS.Transform.toString(transform),
transition,
};
dnd-kit handles this automatically through the useSortable hook's return values.
Think of dnd-kit as a universal power adapter. Every country has different outlet shapes (browsers have different quirks), but the adapter handles the differences. You plug in your device (your drag logic) and it just works, everywhere.
Key Takeaways
-
HTML5 DnD events route through the browser's native drag system. Browsers like Whale can intercept these events for their own gesture features.
-
Pointer Events are low-level input events immune to browser gesture hijacking. pointerdown/pointermove/pointerup deliver raw input without interpretation.
-
dnd-kit uses Pointer Events sensors, not HTML5 DnD. This makes it work in every browser, including Whale, without workarounds.
-
Activation constraints are essential in production. Use distance for desktop and delay + tolerance for mobile to prevent click/scroll/drag conflicts.
-
Accessibility and performance come free. KeyboardSensor gives you keyboard drag support. CSS transform-based movement gives you GPU-accelerated 60fps animation. No extra code needed.
Whale forced me to abandon HTML5 DnD, and I'm grateful it did. What started as a browser compatibility fix turned into better touch support, accessibility, customization, and performance across the board. Sometimes the best upgrades come from the most annoying bugs.