Using the HTML5 Drag and Drop API to implement column reordering

Lately I’ve been building a web-based table component at work and in this post I want to talk about implementing drag and drop column reordering. The HTML5 Drag and Drop API has many quirks that make this harder than expected.

At this point it almost feels mandatory to justify why we are building our own table component in the age of ready made React components and I’d name reasons such as:

Despite us building in Dioxus almost all things in this post should equally work in React and other JS frameworks.

A dedicated drag handle

Because the table header will contain interactive elements, I want a dedicated drag handle that has to be clicked and held down to drag the header. But I can’t just make the handle draggable because then the ghost image would be only of the handle and not the full header.

Demonstration of the drag handle

Luckily this Stack Overflow post has the answer. Set draggable on the header when the handle gets the mousedown event. You also need to remove it when the handle receives mouseup or when the header receives dragend so that the header doesn’t remain draggable on its own.

Drop zones

Simply using the column header elements as drop zones does not work if they have children such as the drag handle because the children will block the dragover and drop events we are interested in. You could set pointer-events: none on the children but this will break interactive elements.

We also have to realize that there are two types of reordering operations we could implement. One column can be dragged onto another column to swap them, or one column can be dragged onto the border between two columns to move it to that location. If a user wants to achieve a desired column order, thinking in terms of moving columns is much easier than thinking about the order in which columns have to be swapped to arrive at the desired state. Therefore we will only implement moving columns and not swapping columns since this is generally what users expect.

Putting things together we know that we need dedicated drop zone elements that are positioned in between columns. We don’t want to waste space in between columns so we will use invisible drop zone elements that are overlayed while the user is in the process of dragging a column.

The fact that there are N columns and N+1 drop zones makes it annoying to render both in the same loop. My first attempt was to work around this by splitting each column header in half to form half of a drop zone.

Attempt to split the header in two

But there is an issue with this approach. Even if the two elements that form one logical drop zone are touching with no gap in between, when moving the mouse between them your code will receive the dragleave event before the dragenter and dragover events. This means that there will be a short period where your code thinks that the mouse left the element which will result in a flicker of the dragover indicator that we will add later.

CSS anchor positioning to the rescue

During work on another part of the project I have learned about the new CSS Anchor Positioning API and already used it to position a popover. Sadly support has not yet landed in Firefox and Safari, but there is a polyfill available. I was really delighted when I realized that anchor positioning is actually the perfect tool to place the drop zones.

On the table header elements set anchor-name: --header-{i+1} to make them available as a reference point to position the drop zones. Then render N+1 drop zones in a separate loop with this styling:

left: anchor(--header-{i} center, 0);    /* left edge of drop zone at center of --header-{i} */
right: anchor(--header-{i+1} center, 0); /* right edge of drop zone at center of  --header-{i+1} */
top: anchor(--header-1 -20%);            /* top edge of drop zone 20% above top of header */
bottom: anchor(--header-1 120%);         /* bottom edge of drop zone 20% below bottom of header */

Anchor positioned drop zones

The second argument to the anchor() function is a fallback in case the anchor does not exist. Because the left anchor of the leftmost drop zone and the right anchor of the rightmost dropzone do not exist, we can set a fallback of 0 to style them as left: 0 and right: 0 respectively. This means that they will extend up to the border of the browser window, which gives the user some margin of error when dropping a column. You could wrap the table into a relative positioned element to cut off the outer drop zones before they reach the border of the browser. I’ve also added a 20% margin above and below the table header to allow some vertical margin of error.

Wrapping up

CSS anchor positioning makes this really elegant to write and I can’t wait for the spec to land in all major browsers. Positioning an element relative to two or more elements was simply not possible before and can be useful in very specific instances such as drag and drop reordering of columns.

All that remains now is to add a dragover indicator to show where the column would be dropped if the mouse was released at the current position and wire up the event handlers:

More details about the event handling can be found in the drag and drop article on web.dev.

Go to the front page