I took a long hard look at what I was trying to do with making components in a list slide to their new location. I figured it out! Behold the smooth movement as items slide into their next position.

There is a lot of stuff going on under the hood here. I’m tracking components with their current and prior locations, and adding some styles at different points in time. I wrapped everything up into a new library so that I can reuse the functionality in other projects.
git: @CodeJamboree/glider
npm: @codejamboree/glider

Microsoft Designer: Image Creator Prompt
Buttons on a web page glide past each other into a new location during a Drag & Drop operation.
On the outside, anyone using the component will need to ensure that they are using keys that identify the data, rather than using the index. Otherwise it will usually appear to the tracking that nothing ever moves, unless some of your items are different sizes. The next thing is that you’ll need to pass a reference, so that the Glider can access the underlying html elements to get the position, read/set style and attributes.
<ul>
<Glider ms={500} className="glider">
{ items.map<MyDataItem>(item => (
<MyListItem
key={item.id}
item={item}
ref={createRef()}
/>
))}
</Glider>
</ul>
By default, it will move items over 500 milliseconds, and apply a “glider” class name during that time. You can override these settings by adding a “ms” and “className” property to the Glider component.
Each component in the Glider list also needs to use that reference object. Normally you can’t access “ref” as a property name. Under the hood, this value is stored in a different place beside the properties, but not within them. To get access to it, you’ll use forwardRef.
export const MyListItem =
forwardRef<RefObject<HTMLLIElement>, Props>(
({ item }, fRef) => {
const ref = fRef as RefObject<HTMLLIElement>;
return <li ref={ref}>{item.name}</li>
}
);
So… what’s going on under the hood? Each time the children of the Glider component changes, I compare all values and determine if any of the x/y coordinates of changed for a given id. If they did, I calculate the offset of where they were previously located. All is not fun in the sun just yet. Before I do anything, I let the browser draw the screen using requestAnimationFrame, and then apply a translation to jump to that position immediately so that it appears as if it hadn’t moved.
Before anything further can be done, I request an animation frame to give the browser a chance to apply the change. Next is the magic. I remove translation, but I change the transition so that the transformation back to the new location takes place over 500 milliseconds.
Now all of this would normally happen within a useEffect call. However, I needed access to the elements updated positions after they have been painted on the screen by the browser. To pull this off, I used the useLayoutEffect instead. It works the same way as useEffect, but after the DOM updates.
There are plenty of things in addition to this overview of whats going on in order to add/remove attributes, preserve styles prior to the movement, and checking if a second glide had started prior to a timeout event firing. There are a few more that I am considering as well in order to optimize it further.
Quirks!
Yes, I ran into a few oddities. First, the item that I was dragging didn’t appear to have any style applied when it moved. Other items that were moving did have the style applied. Instead of applying style to the class name in my cascading style sheets (CSS), I changed it to apply style to any item with the data-glide attribute instead.
[data-glide] {
opacity: 0.5;
}
The next issue I ran into was specific to Drag & Drop. As I’m hovering over the items, and those items move/transition, the Drag & Drop library detected that I had “moved” the dragged item over a new target – when in fact, the target moved itself under my pointer. This resulted in an odd flickering as the two items kept switching between which one was going where… the fix was to disable the item as a drop target while it was moving.
How? Simple. Check for the data-glide attribute. I even took it a step further. The data-glide attribute stores a timestamp of when the glide ends, so if its an old glide operation that somehow didn’t get cleaned up, it was still a valid target.
const canDrop = ({ source }) => {
if (!isDragAndDropItem(source.data)) return false;
const timeout = ref.current?.getAttribute('data-glide');
// not gliding?
if(!timeout) return true;
// still gliding?
if (parseInt(timeout, 10) > Date.now()) return false;
// gliding expired
return true;
}
Well, that’s it. That’s the last part of what I was attempting to figure out with styling Drag & Drop in my application.
