iTranslated by AI
Creating and Reordering Elements with Drag and Drop
Introduction
Whether it's for file uploads or reordering TODO lists, there are many occasions where we use drag and drop.
However, when it came to implementing it myself, I didn't know how to go about it.
That being said, I recently needed to implement a drag-and-drop feature, and while researching, I found a nice module called dnd kit.
So, this time we will look at how to use dnd kit and, as a practical application, create a component that allows elements to be generated via drag and drop.
It's quite long, but please stick with me.
Key Highlights of This Article!!!
The drag-and-drop functionality is implemented using dnd kit.
In addition to drag and drop, it also includes a reordering (sorting) feature.
Leveraging these features, you can dynamically generate components upon dragging and dropping.
You can also reorder the generated elements.
Finished Product
What you can build in this article is the following.

The overall picture of the code is as follows.
A component that performs overall management of drag and drop:
import { DndContext, DragEndEvent, DragOverEvent, DragStartEvent, Modifier } from "@dnd-kit/core";
import { useState } from "react";
import DropComponent from "./DropComponent";
import { arrayMove } from "@dnd-kit/sortable";
import { v4 as uuidv4 } from 'uuid'
import { restrictToVerticalAxis } from "@dnd-kit/modifiers";
import DragComponent from "./DragComponent";
export default function DragDropContext() {
// State for the data of elements displayed in the droppable area
const [elms, setElms] = useState<{ id: string, name: string }[]>([])
// State for the data of the element currently being dragged
const [activeElm, setActiveElm] = useState<{ id: string, name: string, virtualId?: string }>()
// State to determine if the element being dragged belongs to the drop area
const [isDropContent, setIsDropContent] = useState(false)
// State to manage constraints on the dragging direction
// Basically assign values from the "@dnd-kit/modifiers" module
const [modifiers, setModifiers] = useState<Modifier[]>([])
// Function triggered at the start of dragging
const handleDragStart = (event: DragStartEvent) => {
// Get the event related to the dragged element
const { active } = event;
// Determine if the dragged element exists in the drop area
const isExistInDropContent = elms.map((i) => i.id).includes(active.id.toString());
// Create an ID for the dragged element. If it doesn't exist in the drop area, create a new ID.
const id = isExistInDropContent ? active.id.toString() : uuidv4();
// ID of the temporary element generated within the drop area during dragging. undefined when dragging an item already in the drop area.
const virtualId = isExistInDropContent ? undefined : uuidv4();
setActiveElm({ id, name: active.data.current?.name, virtualId })
setIsDropContent(isExistInDropContent)
// If the dragged element is inside the drop area, restrict the movement to the vertical axis only.
isExistInDropContent ? setModifiers([restrictToVerticalAxis]) : setModifiers([])
};
// Function triggered at the end of dragging
const handleDragEnd = (e: DragEndEvent) => {
// Reset the values.
// Since the State setter is asynchronous, resetting the value here won't affect the logic within this function.
setActiveElm(undefined)
// Get the drop area event (over).
const { over } = e;
// If nothing is being dragged or dragging is stopped outside the drop area, abort the process.
if (!over || !activeElm) {
setElms(() => elms.filter((elm) => elm.id !== activeElm?.virtualId))
return
};
// Reflecting the temporary element as an actual element in the drop area
setElms(() => elms.map((elm) => ({
id: elm.id === activeElm.virtualId ? activeElm.id : elm.id,
name: elm.name
})))
// If an element within the drop area was dragged, reorder the elements in the drop area
if (elms.some((elm) => elm.id === activeElm.id)) {
setElms((items) => {
const oldIndex = items.map((i) => i.id).findIndex((val) => val === activeElm.id)
const newIndex = items.map((i) => i.id).findIndex((val) => val === over.id)
return arrayMove(items, oldIndex, newIndex);
});
}
}
const handleDragOver = (e: DragOverEvent) => {
const { over } = e;
// ID of the element that was at the drop location
const overId = over?.id;
// Abort if nothing is being dragged or if the dragged element is being moved outside the drop area.
if (!overId || !activeElm) {
// Delete the generated temporary element
setElms(() => elms.filter((elm) => elm.id !== activeElm?.virtualId));
return;
};
// Add the element once the dragged item enters the drop area
if (activeElm.virtualId && !elms.some((elm) => elm.id === activeElm.virtualId)) {
setElms([...elms.slice(0, elms.length), { id: activeElm.virtualId, name: e.active.data.current?.name }])
}
// Change the position while moving the temporary element within the drop area.
if (elms.some((elm) => elm.id === activeElm.virtualId)) {
setElms((items) => {
const oldIndex = items.map((i) => i.id).findIndex((val) => val === activeElm.virtualId)
const newIndex = items.map((i) => i.id).findIndex((val) => val === over.id)
return arrayMove(items, oldIndex, newIndex);
});
}
}
return (
<>
<div style={{ display: 'flex' }}>
<DndContext modifiers={modifiers} onDragEnd={(e) => handleDragEnd(e)} onDragOver={(e) => handleDragOver(e)} onDragStart={(e) => handleDragStart(e)}>
<DropComponent elms={elms} isDropContent={isDropContent} activeElmId={activeElm?.virtualId}>
</DropComponent>
<DragComponent></DragComponent>
</DndContext>
</div>
</>
)
}
Drop area component:
import { UniqueIdentifier, useDroppable } from "@dnd-kit/core";
import { SortableContext, rectSortingStrategy } from "@dnd-kit/sortable";
import SortDropContent from "./SortDropContent";
type Props = {
elms: { id: UniqueIdentifier; name: string }[];
activeElmId?: string;
isDropContent?: boolean;
};
export default function DropComponent({
elms,
activeElmId,
isDropContent = false,
}: Props) {
const { isOver, setNodeRef: dropRef } = useDroppable({
id: "droppable",
});
const dropStyle = {
padding: "1rem 0 0 0",
minHeight: "100px",
width: "200px",
border: isOver && !isDropContent ? "3px solid blue" : `1px solid`,
};
return (
<div style={{ margin: "2rem" }}>
<div ref={dropRef} style={dropStyle}>
{/* Group of elements within the drop area. By wrapping it in SortableContext, you can configure sorting */}
<SortableContext items={elms} strategy={rectSortingStrategy}>
{elms.map((elm) => {
return (
<SortDropContent
key={elm.id}
elm={elm}
isVirtual={elm.id === activeElmId}
></SortDropContent>
);
})}
</SortableContext>
</div>
</div>
);
}
Component displayed within the drop area:
import { UniqueIdentifier } from "@dnd-kit/core";
import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
type Props = {
elm: { id: UniqueIdentifier; name: string };
isVirtual: boolean;
};
export default function SortDropContent({ elm, isVirtual }: Props) {
const {
isDragging,
// Properties to be set on the sorting handle part
setActivatorNodeRef,
listeners,
// Properties to be set on the entire DOM
setNodeRef,
transform,
transition,
} = useSortable({ id: elm.id });
return (
<div
ref={setNodeRef}
style={{
transform: CSS.Transform.toString(transform),
transition,
}}
>
<p
ref={setActivatorNodeRef}
{...listeners}
style={{
cursor: isDragging ? "grabbing" : "grab",
border: "1px solid",
margin: "0 0 1rem 0",
// Make it semi-transparent if it is a temporary element.
opacity: isVirtual ? ".5" : "1",
}}
>
{elm.name}
</p>
</div>
);
}
Component containing draggable elements:
import { useDraggable } from "@dnd-kit/core";
export default function DragComponent() {
const { attributes, listeners, setNodeRef, transform } = useDraggable({
id: "draggable",
data: { name: "test" },
});
const {
attributes: attributes2,
listeners: listeners2,
setNodeRef: setNodeRef2,
transform: transform2,
} = useDraggable({
id: "draggable2",
data: { name: "test2" },
});
const {
attributes: attributes3,
listeners: listeners3,
setNodeRef: setNodeRef3,
transform: transform3,
} = useDraggable({
id: "draggable3",
data: { name: "test3" },
});
const style = transform
? {
transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`,
}
: undefined;
const style2 = transform2
? {
transform: `translate3d(${transform2.x}px, ${transform2.y}px, 0)`,
}
: undefined;
const style3 = transform3
? {
transform: `translate3d(${transform3.x}px, ${transform3.y}px, 0)`,
}
: undefined;
return (
<div>
<button ref={setNodeRef} style={style} {...listeners} {...attributes}>
テスト
</button>
<button ref={setNodeRef2} style={style2} {...listeners2} {...attributes2}>
テスト2
</button>
<button ref={setNodeRef3} style={style3} {...listeners3} {...attributes3}>
テスト3
</button>
</div>
);
}
Background of Adoption
If you search for drag-and-drop in React, quite a few articles come up.
Comparing the download counts for the following five modules based on this article, react-draggable is the most popular.

Quoted from npm trends search results
So, I thought about trying react-draggable, but as mentioned in the article, it is not being maintained much, and its compatible React versions do not assume the 16 or 18 series.
Looking at package.json for Atlassian's react-beautiful-dnd, it also doesn't seem to support the 18 series.
react-sortable-hoc is the predecessor to dnd kit, so there is no point in using it now.
Therefore, it came down to a choice between react-dnd and dnd kit.
To be honest, either seemed fine, but while react-dnd was last updated two years ago and hasn't seen much activity, dnd kit was updated two months ago and is updated fairly frequently.
Also, when searching on Google for the two modules, it feels like I see more relatively recent articles for dnd kit.
As a result, I adopted dnd kit because I felt it was more likely to be maintained in the future and would respond quickly to updates.
What is dnd kit?
dnd kit is a module with drag-and-drop functionality that can be used with React.
Personally, from looking at the documentation and actually using it, I felt it has the following advantages:
- Lightweight module size of under 10kb.
- Considers ease of use on mobile screens.
- Easy to introduce as drag-and-drop functionality is self-contained within the module.
- Possible to use drag-and-drop functionality with just one context and two functions (hooks).
- Easy to introduce sorting functionality as well.
- Animations are easy to introduce.
While there are many other features and characteristics, these are the points where I felt the most benefit.
Now that I've given a brief overview of dnd kit's features, let's actually try using it.
Basic Implementation
Introducing dnd kit
In a React project, run npm install @dnd-kit/core @dnd-kit/modifiers @dnd-kit/sortable @dnd-kit/utilities.
@dnd-kit/core is the core module that forms the heart of dnd kit, supporting drag-and-drop functionality.
@dnd-kit/modifiers is introduced here to control the dragging direction, as we are doing this time.
Similarly, @dnd-kit/sortable is for sorting functions, and @dnd-kit/utilities is for animation support.
That's all for the installation.
There's no need to prepare separate configuration files; you just write the logic within your components.
It's simple.
Also, while not directly related to drag-and-drop functionality, I am installing the uuid module by running npm install uuid and npm i --save-dev @types/uuid to use UUIDs for the IDs of the elements to be displayed.
Now, let's implement it in the component.
Preparation for Drag-and-Drop and Implementation of Dragging Functionality
Here, we will first perform the basic settings and enable drag-and-drop.
First, let's prepare to use the drag-and-drop functionality for elements.
Write the following functionality in any file:
import { DndContext, useDraggable } from "@dnd-kit/core";
import DraggableBase from "./DraggableBase";
export default function DndContextBase() {
return (
<DndContext>
<DraggableBase></DraggableBase>
</DndContext>
);
}
DndContext is a component implemented using the React Context API to pass data during drag-and-drop operations.
If you want to use the dragging or dropping features of dnd kit, you need to wrap the components you use with DndContext.
Now that drag-and-drop functionality is available, let's implement the dragging feature.
Write the DraggableBase component file as follows:
import { useDraggable } from "@dnd-kit/core";
export default function DraggableBase() {
const { listeners transform } = useDraggable({
id: 'draggableTest',
data: { name: 'test' }
});
const style = transform ? {
transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`,
} : undefined;
return (
<button style={style} {...listeners} >
テスト
</button>
)
}
To use the dragging functionality, use the useDraggable function.
The only thing required to call the useDraggable function is the id property, which uniquely identifies the element.
By setting this, you can use the dragging feature.
However, since we want to generate components using the values of the dragged element later, we also set the data property, which defines the data held by the element.
Regarding the return values of the useDraggable function, only listeners is strictly required to start the dragging functionality.
The listeners must be attached to the element where you want the useDraggable function to detect dragging.
However, even if the movement settings are done, it won't actually appear to move on the screen as it is.
Therefore, we use the transform value.
According to the documentation, transform retrieves the coordinates of the element on the screen, like the following.
By setting this value in CSS, you can move the element.
Now that we've come this far, trying to move it will look like this:

While this is fine just for moving, considering the upcoming drop functionality, we will modify it as follows:
import { useDraggable } from "@dnd-kit/core";
export default function DraggableBase() {
const { listeners, setNodeRef, transform } = useDraggable({
id: "draggableTest",
data: { name: "test" },
});
const style = transform
? {
transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`,
}
: undefined;
return (
<button ref={setNodeRef} style={style} {...listeners}>
テスト
</button>
);
}
The point to note is that the value setNodeRef is set in the ref attribute.
Without this, it wouldn't be able to detect collisions with other elements, which would affect the functionality we want to implement later.
Therefore, we will at least add setNodeRef before implementing the drop functionality.
Dropping Functionality
Now that we can drag elements, next we will implement the dropping functionality.
Create a component file like the following:
import { useDroppable } from "@dnd-kit/core";
export default function DrppabbleBase() {
const { isOver, setNodeRef } = useDroppable({
id: "droppableTest",
});
const dropStyle = {
marginTop: "1rem",
padding: "1rem 0 0 0",
minHeight: "100px",
width: "200px",
border: isOver ? "3px solid blue" : `1px solid`,
};
return <div style={dropStyle} ref={setNodeRef}></div>;
}
The point to note is the useDroppable function.
Components where this function is configured can possess dropping functionality.
The useDroppable function requires an ID as an argument to uniquely identify the drop-target element.
While an ID is mandatory, we won't be using any data for the drop functionality this time, so we will omit the data property.
As return values, we obtain isOver and setNodeRef.
setNodeRef is essential for detecting when a dragged element has entered the drop area, so it is basically a required value to retrieve.
isOver becomes true when an element is being dragged over the drop area.
I retrieved it this time because I want to apply a style when the dragged element is over the drop area.
Now that the drop component is created, let's call it from the parent side.
import { DndContext, useDraggable } from "@dnd-kit/core";
import DraggableBase from "./DraggableBase";
import DrppabbleBase from "./DropabbleBase";
export default function DndContextBase() {
return (
<DndContext>
<DraggableBase></DraggableBase>
<DrppabbleBase></DrppabbleBase>
</DndContext>
);
}
Once you have done this, the border will turn blue when the dragged element is over the drop area, as shown below.

Generating Elements in the Drop Area When a Dragged Element is Dropped
With the previous two sections, we were able to implement basic drag-and-drop functionality. Therefore, as a slightly more advanced application, we will now make it so that an element is generated when a dragged element is dropped within the drop area.
First, I will show the completed code. To start with, here is the DndContextBase function that calls DndContext.
import { DndContext, DragEndEvent } from "@dnd-kit/core";
import DraggableBase from "./DraggableBase";
import DrppabbleBase from "./DropabbleBase";
import { useState } from "react";
import { v4 as uuidv4 } from 'uuid'
export default function DndContextBase() {
const [items, setItems] = useState<{ id: string, name: string }[]>([])
const handleDragEnd = (event: DragEndEvent) => {
if (event.over) {
setItems((prev) => [...prev, { id: uuidv4(), name: event.active.data.current?.name }])
}
}
return (
<DndContext onDragEnd={handleDragEnd}>
<DraggableBase></DraggableBase>
<DrppabbleBase items={items}></DrppabbleBase>
</DndContext>
)
}
Next is the code inside the DrppabbleBase function, which represents the drop area.
import { useDroppable } from "@dnd-kit/core";
type Props = {
items: { id: string, name: string }[],
};
export default function DrppabbleBase({ items }: Props) {
const { isOver, setNodeRef } = useDroppable({
id: "droppableTest",
});
const dropStyle = {
marginTop: "1rem",
padding: "1rem 0 0 0",
minHeight: "100px",
width: "200px",
border: isOver ? "3px solid blue" : `1px solid`,
};
return (
<div style={dropStyle} ref={setNodeRef}>
{items.map((item) => (
<div style={{ marginBottom: ".5rem" }} key={item.id}>
<button>{item.name}</button>
</div>
))}
</div>
);
}
Now, let's go through the explanation. In this topic, the most important part is the following function snippet.
const handleDragEnd = (event: DragEndEvent) => {
if (event.over) {
setItems((prev) => [
...prev,
{ id: uuidv4(), name: event.active.data.current?.name },
]);
}
};
According to the documentation, dnd kit has four main event handlers:
- onDragStart: Executed when a draggable element is dragged.
- onDragMove: Fired while a dragged element is moving.
- onDragOver: Fired when a dragged element moves over a droppable area.
-
onDragEnd: Fired when a dragged element is dropped.
In this case, the requirement is to generate an element when the dragged element is dropped in a droppable area, so we use the fourth handler, onDragEnd.
The function for that is thehandleDragEndfunction mentioned earlier.
Let's look into the contents of this function a bit more.
TheDragEndEventtype argument has the following properties:
interface DragEvent {
activatorEvent: Event;
active: Active;
collisions: Collision[] | null;
delta: Translate;
over: Over | null;
}
export interface DragEndEvent extends DragEvent {}
And what we are using this time are the active and over properties.
The active property holds information about the element that was being dragged.
The over property holds information about the drop area if the element was dropped over it; otherwise, it is set to null.
From this explanation, we can infer that the reason for the conditional branch is to add a value to the State only when the dragged element is released in the drop area.
if (event.over) {
setItems((prev) => [
...prev,
{ id: uuidv4(), name: event.active.data.current?.name },
]);
}
Actually, the behavior without the above branch would be as follows.

An element would be generated in the drop area even though it wasn't released there.
To prevent this, we ensure the processing occurs only when dropped within the drop area.
Then, when dropped within the drop area, information is retrieved from the active property (which holds information about the dragged element), an ID is generated, and it is added to the State.
Note that regarding the data.current part of the active property, the value set in the data property when calling the useDraggable function is stored here.
Since we defined it as follows this time, we specify name to retrieve the information.
useDraggable({
id: "draggableTest",
data: { name: "test" },
});
Now that we've looked at the contents of the handleDragEnd function, we just need to assign this function to the onDragEnd attribute of DndContext.
The rest is not specific to dnd kit.
We pass the State to the drop area component and generate elements using the map function within the drop area.
By doing the above, an element will be generated when you release the dragged element in the drop area.

You can confirm that the element is generated only when the dragged element is released within the drop area.
Aside: Making the drag element's data attribute type-safe
Earlier, we looked at how to generate elements, but the name part in event.active.data.current?.name used to retrieve the drag element's information does not support autocompletion.
Therefore, in the previous example, I manually checked the value set in the useDraggable function and typed it in by hand.
This is prone to errors and doesn't take advantage of the benefits of using TypeScript.
So, referring to this issue, I used generics to make it extensible, as shown in the code below.
import {
Active,
Collision,
Data,
DndContextProps,
Over,
Translate,
UseDraggableArguments,
UseDroppableArguments,
useDraggable as useOriginalDraggable,
useDroppable as useOriginalDroppable,
} from "@dnd-kit/core";
export interface UseDroppableTypesafeArguments<DRO> extends Omit<UseDroppableArguments, "data"> {
data: DRO;
}
export function useBaseDroppable<DRO extends Data>(props: UseDroppableTypesafeArguments<DRO>) {
return useOriginalDroppable(props);
}
export interface UseDraggableTypesafeArguments<DRA> extends Omit<UseDraggableArguments, "data"> {
data: DRA;
}
export function useBaseDraggable<DRA extends Data>(props: UseDraggableTypesafeArguments<DRA>) {
return useOriginalDraggable(props);
}
interface TypesafeActive<DRA> extends Omit<Active, "data"> {
data: React.MutableRefObject<DRA | undefined>;
}
interface TypesafeOver<DRO> extends Omit<Over, "data"> {
data: React.MutableRefObject<DRO | undefined>;
}
interface DragEvent<DRA, DRO> {
activatorEvent: Event;
active: TypesafeActive<DRA>;
collisions: Collision[] | null;
delta: Translate;
over: TypesafeOver<DRO> | null;
}
export interface DragStartEvent<DRA, DRO> extends Pick<DragEvent<DRA, DRO>, "active"> { }
export interface DragMoveEvent<DRA, DRO> extends DragEvent<DRA, DRO> { }
export interface DragOverEvent<DRA, DRO> extends DragMoveEvent<DRA, DRO> { }
export interface DragEndEvent<DRA, DRO> extends DragEvent<DRA, DRO> { }
export interface DragCancelEvent<DRA, DRO> extends DragEndEvent<DRA, DRO> { }
export interface DndContextTypesafeProps<DRA, DRO>
extends Omit<
DndContextProps,
"onDragStart" | "onDragMove" | "onDragOver" | "onDragEnd" | "onDragCancel"
> {
onDragStart?(event: DragStartEvent<DRA, DRO>): void;
onDragMove?(event: DragMoveEvent<DRA, DRO>): void;
onDragOver?(event: DragOverEvent<DRA, DRO>): void;
onDragEnd?(event: DragEndEvent<DRA, DRO>): void;
onDragCancel?(event: DragCancelEvent<DRA, DRO>): void;
}
Although there is quite a lot written here, what I'm doing is simply using Omit to remove the data property and adding a custom data property with a generic type.
This allows for autocompletion of the set types in various events within DndContext, and it also makes it harder for discrepancies to occur in the data attributes set in the useDraggable and useDroppable functions.
Now, let's actually use it.
Write the following code in an arbitrary file:
import {
UseDraggableTypesafeArguments,
useBaseDraggable,
DndContextTypesafeProps,
} from "./wrap-dnd-kit";
type BtnData = { name: string };
export interface BtnDndContextTypesafeProps
extends DndContextTypesafeProps<BtnData, {}> {}
export const useBtnDraggable = (
props: UseDraggableTypesafeArguments<BtnData>
) => useBaseDraggable < BtnData > props;
This defines the types for the props passed to DndContext and an extended version of the useBaseDraggable function with the data property typed.
After defining this, change the DndContext as follows:
import { DndContext, DragEndEvent } from "@dnd-kit/core";
import DraggableBase from "./DraggableBase";
import DrppabbleBase from "./DropabbleBase";
import { useState } from "react";
import { v4 as uuidv4 } from 'uuid'
import { BtnDndContextTypesafeProps } from "./useBtnDndKit";
export default function DndContextBase() {
const [items, setItems] = useState<{ id: string, name: string }[]>([])
const contextProps: BtnDndContextTypesafeProps = {
onDragEnd: (event) => {
const dragName = event.active.data.current?.name;
if (event.over && dragName) {
setItems((prev) => [...prev, { id: uuidv4(), name: dragName }])
}
}
}
return (
<DndContext {...contextProps}>
<DraggableBase></DraggableBase>
<DrppabbleBase items={items}></DrppabbleBase>
</DndContext>
)
}
The props to be passed to DndContext are defined as an object and then passed using spread syntax.
The content of the onDragEnd property is almost the same as before, but since it's properly typed, we can now remove undefined and save it to the State.
While this alone works with type completion, it's risky if it deviates from the data property defined in the useDraggable function, so we'll change the DraggableBase function as follows:
import { useBtnDraggable } from "./useBtnDndKit";
export default function DraggableBase() {
const { listeners, setNodeRef, transform } = useBtnDraggable({
id: "draggableTest",
data: { name: "test" },
});
const style = transform
? {
transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`,
}
: undefined;
return (
<button ref={setNodeRef} style={style} {...listeners}>
テスト
</button>
);
}
By using the useBtnDraggable function, the data property can only be defined with the type {name: string}.
This allows for type completion and makes type discrepancies less likely to occur.
While there are still areas for improvement, being able to type event.active.data.current is a significant gain.
In the future, when using multiple drag-and-drop features, I hope to utilize these definitions for properties that don't yet exist.
Reordering elements within the drop area
We have been able to generate elements via drag and drop.
However, there are times when you generate an element in the drop area, but you want to swap the order because it wasn't exactly where you intended.
dnd kit also caters to such needs.
So, let's implement element reordering here.
First, here is the entire completed code.
The main DndContextBase function will look like this:
import { DndContext, DragEndEvent } from "@dnd-kit/core";
import DraggableBase from "./DraggableBase";
import DrppabbleBase from "./DropabbleBase";
import { useState } from "react";
import { v4 as uuidv4 } from 'uuid'
import { BtnDndContextTypesafeProps, BtnDragEndEvent } from "./useBtnDndKit";
import { arrayMove } from "@dnd-kit/sortable";
export default function DndContextBase() {
const [items, setItems] = useState<{ id: string, name: string }[]>([])
const handleSort = (event: BtnDragEndEvent) => {
const { active, over } = event;
setItems((items) => {
const oldIndex = items.findIndex((item) => item.id === active.id)
const newIndex = items.findIndex((item) => item.id === over?.id)
return arrayMove(items, oldIndex, newIndex);
});
}
const addItem = (event: BtnDragEndEvent) => {
const dragName = event.active.data.current?.name;
if (event.over && dragName) {
setItems((prev) => [...prev, { id: uuidv4(), name: dragName }])
}
}
const contextProps: BtnDndContextTypesafeProps = {
onDragEnd: (event) => {
addItem(event);
handleSort(event)
}
}
return (
<DndContext {...contextProps}>
<DraggableBase></DraggableBase>
<DrppabbleBase items={items}></DrppabbleBase>
</DndContext>
)
}
Add the types related to DragEndEvent to the wrapper file created in the "Aside" section earlier.
import {
UseDraggableTypesafeArguments,
useBaseDraggable,
DndContextTypesafeProps,
DragEndEvent,
} from "./wrap-dnd-kit";
type BtnData = { name: string };
export interface BtnDndContextTypesafeProps
extends DndContextTypesafeProps<BtnData, {}> {}
export interface BtnDragEndEvent extends DragEndEvent<BtnData, {}> {}
export const useBtnDraggable = (
props: UseDraggableTypesafeArguments<BtnData>
) => useBaseDraggable < BtnData > props;
Set up the sortable area in the drop area component.
import { useDroppable } from "@dnd-kit/core";
import { SortableContext } from "@dnd-kit/sortable";
import SortableBase from "./SortableBase";
export type Props = {
items: { id: string, name: string }[],
};
export default function DrppabbleBase({ items }: Props) {
const { isOver, setNodeRef } = useDroppable({
id: "droppableTest",
});
const dropStyle = {
marginTop: "1rem",
padding: "1rem 0 0 0",
minHeight: "100px",
width: "200px",
border: isOver ? "3px solid blue" : `1px solid`,
};
return (
<div style={dropStyle} ref={setNodeRef}>
<div
style={{
display: "flex",
flexDirection: "column",
justifyContent: "center",
gap: ".5rem",
}}
>
<SortableContext items={items}>
{items.map((item) => (
<SortableBase
key={item.id}
id={item.id}
name={item.name}
></SortableBase>
))}
</SortableContext>
</div>
</div>
);
}
Create the elements for sorting.
import { useSortable } from "@dnd-kit/sortable";
type Props = { id: string, name: string };
export default function SortableBase({ id, name }: Props) {
const { setNodeRef, listeners } = useSortable({ id });
return (
<button ref={setNodeRef} {...listeners}>
{name}
</button>
);
}
Finally, although not directly related to the sorting feature, add a draggable element to make it easier to understand that sorting has taken place.
import { useBtnDraggable } from "./useBtnDndKit";
export default function DraggableBase() {
const { listeners, setNodeRef, transform } = useBtnDraggable({
id: "draggableTest",
data: { name: "test" },
});
const {
listeners: listeners2,
setNodeRef: setNodeRef2,
transform: transform2,
} = useBtnDraggable({
id: "draggableTest2",
data: { name: "test2" },
});
const style = transform
? {
transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`,
}
: undefined;
const style2 = transform2
? {
transform: `translate3d(${transform2.x}px, ${transform2.y}px, 0)`,
}
: undefined;
return (
<>
<button ref={setNodeRef} style={style} {...listeners}>
テスト
</button>
<button ref={setNodeRef2} style={style2} {...listeners2}>
テスト2
</button>
</>
);
}
By doing this, you can now sort as follows:

Now, let's look at the contents of the code.
First is securing the sorting area.
By wrapping child elements in the SortableContext component as follows, those elements become sortable.
<SortableContext items={items}>
{items.map((item) => (
<SortableBase key={item.id} id={item.id} name={item.name}></SortableBase>
))}
</SortableContext>
However, the items attribute is required.
As can be seen from the following type definition, this items attribute stores an array of string or number types, or an array of objects having an id property.
export interface Props {
items: (UniqueIdentifier | {
id: UniqueIdentifier;
})[]
//...omitted
}
export declare type UniqueIdentifier = string | number;
This attribute is a mandatory property because it determines which element in the sorting area is at which position.
Since the sorting area is secured, let's look at the sorting elements.
For sorting elements, call the useSortable function as follows and attach the necessary attributes to the elements you want to move.
const { setNodeRef, listeners } = useSortable({ id });
return (
<button ref={setNodeRef} {...listeners}>
{name}
</button>
);
Pass a unique ID to the useSortable function so the sorting element can be identified, and attach setNodeRef for collision detection and listeners for event detection.
The usage itself is mostly the same as for drag and drop.
With the above, the preparation for sorting is complete.
However, this alone is not enough to perform sorting.
This is because the sorting logic itself needs to be performed in the onDragEnd attribute of the DndContext component.
Therefore, the process in the onDragEnd property of the DndContextBase function has been split into functions and changed as follows:
const handleSort = (event: BtnDragEndEvent) => {
const { active, over } = event;
setItems((items) => {
const oldIndex = items.findIndex((item) => item.id === active.id);
const newIndex = items.findIndex((item) => item.id === over?.id);
return arrayMove(items, oldIndex, newIndex);
});
};
const addItem = (event: BtnDragEndEvent) => {
const dragName = event.active.data.current?.name;
if (event.over && dragName) {
setItems((prev) => [...prev, { id: uuidv4(), name: dragName }]);
}
};
const contextProps: BtnDndContextTypesafeProps = {
onDragEnd: (event) => {
addItem(event);
handleSort(event);
},
};
What is important here is the arrayMove function.
Checking the source code, it is as follows:
/**
* Move an array item to a different position. Returns a new array with the item moved to the new position.
*/
export function arrayMove<T>(array: T[], from: number, to: number): T[] {
const newArray = array.slice();
newArray.splice(
to < 0 ? newArray.length + to : to,
0,
newArray.splice(from, 1)[0]
);
return newArray;
}
If you specify the index number where the element you want to move exists as the second argument and the destination index number as the third argument, it seems to manipulate the array nicely.
Therefore, this time I use the findIndex method to get the index number of the element currently being dragged, and similarly use the findIndex method to get the index number of the element that was underneath when the dragged element was released.
const oldIndex = items.findIndex((item) => item.id === active.id);
const newIndex = items.findIndex((item) => item.id === over?.id);
Note that the over property retrieves the value of the topmost element for which setNodeRef is configured.
Therefore, if there are elements as shown below, and you release the dragged element over Element ②, the over property will retrieve the value of Element ②.

On the other hand, even if you release the dragged element over Element ③, since there is no setNodeRef, the over property will not retrieve the value of Element ③.
Instead, since Element ①, which surrounds Element ③, has setNodeRef configured, the over property retrieves the value of Element ①.
Thus, you can understand that the code above reorders by moving the dragged element to the index number of the sortable element that was in the dropped area, and incrementing the index number of elements after that index number by one.
setItems((items) => {
const oldIndex = items.findIndex((item) => item.id === active.id);
const newIndex = items.findIndex((item) => item.id === over?.id);
return arrayMove(items, oldIndex, newIndex);
});
With the above, we have been able to implement basic drag-and-drop functionality and element reordering.
So, from here, I will explain the implementation of the movement shown initially, utilizing other functions provided by dnd kit.

Explanation of the Completed Code
Root Part
First, I will explain the root part where DndContext is used.
// State for the data of elements displayed in the droppable area
const [elms, setElms] = useState<{ id: string, name: string }[]>([])
// State for the data of the element currently being dragged
const [activeElm, setActiveElm] = useState<{ id: string, name: string, virtualId?: string }>()
// State to determine if the element being dragged belongs to the drop area
const [isDropContent, setIsDropContent] = useState(false)
// State to manage constraints on the dragging direction
// Basically assign values from the "@dnd-kit/modifiers" module
const [modifiers, setModifiers] = useState<Modifier[]>([])
These are basically state definitions.
The content is as described in the comments, but there is one supplementary point.
The state defining the Modifier type array at the bottom stores values for a feature called Modifiers provided by dnd kit.
Modifiers, to put it very simply, are values that limit the directions in which a draggable element can move.
By defining it in DndContext as follows, you can limit the movement direction of the internal drag elements.
<DndContext modifiers={modifiers}>//...omitted</DndContext>
Next, let's look at the process when a drag element is grabbed.
// Function triggered at the start of dragging
const handleDragStart = (event: DragStartEvent) => {
// Get events related to the dragged element
const { active } = event;
// Determine if the dragged element exists in the drop area
const isExistInDropContent = elms
.map((i) => i.id)
.includes(active.id.toString());
// Create an ID for the dragged element. If it doesn't exist in the drop area, create a new ID.
const id = isExistInDropContent ? active.id.toString() : uuidv4();
// ID of the temporary element generated within the drop area during dragging. undefined when dragging an item already in the drop area.
const virtualId = isExistInDropContent ? undefined : uuidv4();
setActiveElm({ id, name: active.data.current?.name, virtualId });
setIsDropContent(isExistInDropContent);
// If the dragged element is inside the drop area, restrict movement to the vertical axis only
isExistInDropContent
? setModifiers([restrictToVerticalAxis])
: setModifiers([]);
};
The logic is as described in the comments, but here we are storing the dragged element in the state and changing the values based on whether the dragged element is from the drop area or not.
A supplementary explanation for the virtualId part:
This field is provided because we want to be able to create and move elements within the drop area even while dragging, but we want to generate it as a normal element with a real ID once it is released.
If we use only id instead of virtualId, an extra element would be generated when dragging stops, in addition to the element created during the drag movement.
To prevent this, we use virtualId for temporary generation until the final drop occurs, and use the real id when it is officially generated.
This is why we provide a virtualId.
Next, regarding the process executed when a drag element comes into contact with an element in the drop area:
const handleDragOver = (e: DragOverEvent) => {
const { over } = e;
// ID of the element that was at the drop location
const overId = over?.id;
// Abort if nothing is being dragged or if the dragged element is being moved outside the drop area.
if (!overId || !activeElm) {
// Delete the generated temporary element
setElms(() => elms.filter((elm) => elm.id !== activeElm?.virtualId));
return;
}
// If the drag element enters the drop area and doesn't already exist, add the element
if (
activeElm.virtualId &&
!elms.some((elm) => elm.id === activeElm.virtualId)
) {
setElms([
...elms.slice(0, elms.length),
{ id: activeElm.virtualId, name: e.active.data.current?.name },
]);
}
// Change position while moving the temporary element within the drop area.
if (elms.some((elm) => elm.id === activeElm.virtualId)) {
setElms((items) => {
const oldIndex = items
.map((i) => i.id)
.findIndex((val) => val === activeElm.virtualId);
const newIndex = items
.map((i) => i.id)
.findIndex((val) => val === over.id);
// Reorder elements to the specified location
return arrayMove(items, oldIndex, newIndex);
});
}
};
This part handles the temporary generation of elements during dragging and the logic for changing their positions.
Specifically, it corresponds to the following behavior.

Let's also check the process for when the drag element is released, which is the last event handler.
// Function triggered at the end of dragging
const handleDragEnd = (e: DragEndEvent) => {
// Reset values.
// Since state updates are asynchronous, resetting values here doesn't affect the logic within this function.
setActiveElm(undefined);
// Get drop area event (over).
const { over } = e;
// If nothing is being dragged or dragging is stopped outside the drop area, abort the process.
if (!over || !activeElm) {
// Delete the temporary element if it was generated
setElms(() => elms.filter((elm) => elm.id !== activeElm?.virtualId));
return;
}
// Reflect the temporary element as an actual element in the drop area
setElms(() =>
elms.map((elm) => ({
id: elm.id === activeElm.virtualId ? activeElm.id : elm.id,
name: elm.name,
}))
);
// If an element within the drop area was dragged, reorder the elements in the drop area
if (elms.some((elm) => elm.id === activeElm.id)) {
setElms((items) => {
const oldIndex = items
.map((i) => i.id)
.findIndex((val) => val === activeElm.id);
const newIndex = items
.map((i) => i.id)
.findIndex((val) => val === over.id);
return arrayMove(items, oldIndex, newIndex);
});
}
};
There is similar logic in the final part, but this is to enable actual movement when sorting within the drop area.
The logic written for handleDragOver was for reordering temporary elements, whereas this is written to reorder the elements that have been officially generated, as seen here:

These are the primary functions for the part calling DndContext.
Next, I would normally explain the component containing the draggable elements, but since there's nothing more than what was already covered in the basic implementation, I'll omit that.
Drop Area Component
As for the drop area component, there isn't much to explain as most of it was covered in the basic implementation. However, I'll add a note about the following style definition.
const dropStyle = {
padding: "1rem 0 0 0",
minHeight: "100px",
width: "200px",
border: isOver && !isDropContent ? "3px solid blue" : `1px solid`,
};
Regarding the border part, I've made it so that the blue border appears only when an element from outside the drop area enters it.

I set this up because I thought it would make it easier to see that the element has entered the drop area. However, since having a blue border in other conditions might interfere with the operation, I've designed it not to change the border otherwise.
Sortable Elements
Finally, here is the component for sortable elements. Although I've already mentioned the basic functionality, I'll supplement it with the following part.
import { CSS } from "@dnd-kit/utilities";
//...omitted
const {
//...omitted
transform,
transition,
} = useSortable({ id: elm.id });
return (
<div
style={{
transform: CSS.Transform.toString(transform),
transition,
}}
>
//...omitted
</div>
);
In the basic implementation, reordering itself worked, but there were no animations during the change, making it difficult at first glance to tell if reordering was actually happening.
To address this, adding animations is one solution.
The addition of transform and transition provides this animation.
By using these values, animation is applied to the reordering, making it easier to understand the current state of sorting.
Note that for the transform that handles position changes, simply applying the value from the useSortable function as-is won't work correctly.
As shown below, while it holds coordinate values, it is not in the correct format for the transform property.
export declare type Transform = {
x: number;
y: number;
scaleX: number;
scaleY: number;
};
Therefore, by using the toString method of the CSS object from @dnd-kit/utilities, the retrieved coordinates are converted into a format that works correctly with the transform property.
Conclusion
In this article, I have explained the drag-and-drop functionality using dnd kit and provided an explanation of the code for generating elements through drag and drop.
It turned out to be quite a long post, but I'm satisfied that I could cover the basic features.
Please note that what we implemented this time is strictly basic behavior.
There are still many more features and potential use cases, so if you're interested, please do give them a try.
Thank you very much for reading this far.
Discussion
マロンさん
ソートするドラッグ要素と、ドロップ領域の実装で分からないところがあり、大変参考になりました!ありがとうございます。
実際に動かして動作を確認してみたいのですが、サンプルソースを公開されていたりはされてませんでしょうか?
ご検討、よろしくお願いいたします。
コメントありがとうございます!
参考になってとても嬉しいです。
サンプルコードについて承知しました。
後ほどプロジェクトリンクを作成します。
ただ、色々試しているプロジェクトの中に混ぜ込んでいるものになっているため、分離作業を行います。
そのため、少しお待ちいただけますと幸いです。
よろしくお願いします。