TLDR: dnd-kit's
closestCenteris a geometry function. It doesn't know what you mean. When you have nested droppables, you have to write that intent yourself.
The Setup
I'm building the scheduling board for an ecommerce business's KOL CRM — a drag-and-drop calendar where you pull "pieces" (KOL guests, webinar topics, traffic sources) out of a tray and drop them onto dates.
Each calendar cell has two layers of droppable: a day-cell (the full-width date container) and one or more combo-slots (the webinar cards nested inside). The idea is simple — you drag a KOL piece onto an existing webinar card, and it slots in. Done.
Except that's not what happened.
The Wall
Out of the box, dnd-kit (my drag-and-drop library of choice) uses closestCenter for collision detection. Makes sense in theory: find whichever droppable's center is nearest to the drag pointer, call that the target.
The problem? The day-cell is enormous. It spans the entire column. The combo-slot inside it is small — maybe a quarter of the height. closestCenter doesn't know that the combo-slot is the intended target. It just sees a bigger rectangle and lands there instead.
So I'd drag a KOL piece toward a webinar card, release it, and the drop registered on the day-cell — not the card. The piece created a whole new webinar instead of filling the existing one. Every time.
What I Tried First (The Wrong Turn)
I spent a frustrating stretch chasing a CSS-perf red herring. I'd added will-change: transform to the drag overlay for smooth GPU compositing — totally reasonable optimization. But it was silently interfering with how dnd-kit calculates droppable positions during the drag. The drag-to-slot just stopped working entirely.
Removing will-change: transform gave me drag-to-slot back. That was real. But it didn't fix the collision problem — I'd just cleared the fog enough to see the actual bug.
The Fix That Worked
Custom collision detection. dnd-kit lets you swap in your own function, and that's exactly what I needed.
The logic:
- If a combo-slot is in the active collision set, always prefer it over a day-cell. Smaller, more specific target wins.
- If only a day-cell collides, fall through to it — but treat it as "drop on this date's existing webinar" rather than "create a new one."
That second part was its own fix: smart slot fill — when you drop a piece on a bare day-cell, it checks whether a webinar already exists for that date. If yes, fill the empty slot. If no, then create a new one.
And one more rule baked into the data layer: one webinar per day. No more accidentally stacking two webinars on the same date because you slightly missed the card.
Why This Matters
Default collision detection is a geometry function. It treats every droppable as equal area with equal priority. That's fine for flat lists, kanban columns, sortable grids — anything where the targets are peers.
It breaks the moment you have nested droppables with different intent. The big container is "somewhere in this zone." The small target is "this specific thing." Your users think in terms of the specific thing. The algorithm thinks in terms of the big rectangle.
You have to encode what a drop means — not just where the pointer happened to be. Once I wrote that into the collision function, the board started behaving exactly like I'd imagined it. Users just… drop stuff. It goes where it should.
That's the bar worth hitting.
P.S. The
will-change: transformtrap is worth remembering: GPU compositing hints can shift how dnd-kit measures droppable rects mid-drag. If your drag lands in the wrong place and you've added CSS perf optimizations recently, check that first.