DnD Kit is a relatively new, hook-based library that makes it easy to create performant and accessible drag-and-drop UI interactions in React. I have been using the library a lot at Super, particularly in our new dashboard, and I wanted to share some examples of the kind of interactions that can be built with it.
In this post, I will provide a brief overview of the core concepts behind @dnd-kit and then i’ll move on to a number of code examples with explanations. If you need more detail about anything i cover, you can always check out the documentation here.
Core Concepts
Drag-and-drop interactions on the web typically involve three elements:
a draggable component that can be dragged,
a droppable area where draggable components can be dropped over, and
a system that manages the interaction between the draggable components and the droppable areas.
To see how these elements typically come together, you can play with the demo below:
OL
In the demo, we can drag our draggable components and drop them anywhere on the page. However, we can also restrict the dropping of our draggable components to particular areas:
Q1
I2
P3
G4
DndKit is designed around this approach, providing three core primitives exported from the @dnd-kit/core package:
The useDraggable hook - A React hook which makes HTML elements draggable.
The useDroppable hook - A React hook which makes HTML elements droppable areas.
The DndContext - A context provider which manages the interaction between draggable and droppable components using the React Context API.
Drag-and-drop interactions often require more than just dragging and dropping components. For example, take a look at the demo below:
SORTABLE 1
SORTABLE 2
SORTABLE 3
SORTABLE 4
In the demo, we have a list. When we drag a single item, it automatically sorts with its other siblings. This type of interaction is known as a sortable interface, where components are both draggable and droppable. We can refer to these components as "Sortable Components".
Sortable components can be used in a variety of ways. For instance, we can have multiple containers with sortable list items, enabling users to sort the containers, list items, and drag items between them:
CONTAINER A
A1
A2
A3
CONTAINER B
B1
B2
B3
To handle this use case, @dnd-kit exposes a set of presets from the @dnd-kit/sortable package:
The useSortable hook - A react hook which makes HTML elements sortable. The hook is a combination of the useDraggable and useDroppable hooks.
The SortableContext - An additional context provider which manages the interactions between sortable components.
Let's now explore how to use the primitives and presets to create various drag-and-drop interactions.
Examples
We'll be looking at four common types of drag and drop interactions:
Free Drag-and-Drop - Drag draggable components across the entire page, similar to apps such as Figma and Excalidraw.
Droppable Containers - Drag multiple draggable components into one or more designated droppable containers.
Single Container Sortable List - A sortable list within a single container.
Multi-Container Sortable List - Sortable lists within multiple sortable containers.
Free Drag-and-Drop
A user interface that allows users to pick up and drag an element across the entire page.
importReactfrom"react";importtype{DragEndEvent}from"@dnd-kit/core";import{DndContext,KeyboardSensor,MouseSensor,TouchSensor,useSensor,useSensors}from"@dnd-kit/core";import"./styles.css";import{Draggable}from"./Draggable";constdraggable = [{id:"DG",name:"D",position:{x:24,y:24}},{id:"KA",name:"K",position:{x:164,y:164}}];functionApp(){//Sensors are an abstraction to detect different input methods in//order to initiate drag operations, respond to movement and end or//cancel the operation. See more here: https://docs.dndkit.com/api-documentation/sensorsconstsensors = useSensors(useSensor(KeyboardSensor),useSensor(TouchSensor),useSensor(MouseSensor));const[draggables,setDraggables] = React.useState([...draggable]);return(<divstyle={{width:"100%",minHeight:"100vh"}}className="dotted-bg"><DndContextonDragEnd={handleDragEnd}sensors={sensors}>{draggables.map((draggable)=>(<Draggable{...draggable}key={draggable.id}//Pass current position as css styles, makes sure we can drag element//across the viewportstyles={{position:"absolute",left:`${draggable.position.x}px`,top:`${draggable.position.y}px`}}/>))}</DndContext></div>);functionhandleDragEnd(ev: DragEndEvent){//Get the id of the active draggableconstactiveId = ev.active.id;//Update the statesetDraggables((draggables)=>{returndraggables.map((draggable)=>{//if draggable id matches the active idif(draggable.id === activeId){return{...draggable,//update its position with the new position in the delta object in//the drag end eventposition:{x:draggable.position.x += ev.delta.x,y:draggable.position.y += ev.delta.y}};}//return draggable that is not activereturndraggable;});});}}exportdefaultApp;
Droppable Containers
A user interface that allows users to pick up and drag an element into designated droppable areas
importReactfrom"react";import{DragOverEvent,DragStartEvent}from"@dnd-kit/core";import{DndContext,KeyboardSensor,MouseSensor,TouchSensor,useSensor,DragOverlay,useSensors}from"@dnd-kit/core";import"./styles.css";import{Draggable}from"./Draggable";import{Droppable}from"./Droppable";import{items,Item}from"./items";functionApp(){//Sensors are an abstraction to detect different input methods in//order to initiate drag operations, respond to movement and end or//cancel the operation. See more here: https://docs.dndkit.com/api-documentation/sensorsconstsensors = useSensors(useSensor(KeyboardSensor),useSensor(TouchSensor),useSensor(MouseSensor));const[draggables,setDraggables] = React.useState([...items]);const[containers] = React.useState(["A","B"]);//The state of the active draggable, used to render the drag overlay belowconst[activeItem,setActiveItem] = React.useState<Item | undefined>(undefined);return(<DndContextsensors={sensors}onDragOver={handleDragOver}onDragStart={handelDragStart}><divclassName="app"><divclassName="containers">{containers.map((id)=>(<Droppablekey={id}id={id}className="droppable">{draggables//render draggables within thier respective containers
.filter((draggable)=>draggable.containerId === id)
.map((draggable)=>(<Draggablekey={draggable.id}{...draggable}/>))}</Droppable>))}</div><Droppableid="ROOT"className="root-droppable">{draggables//only render draggables with have a containerId of ROOT
.filter((draggable)=>draggable.containerId === "ROOT")
.map((draggable)=>(<Draggablekey={draggable.id}{...draggable}/>))}</Droppable></div><DragOverlay>{activeItem && (//Render a drag overlay when using multiple containers// check here https://docs.dndkit.com/api-documentation/draggable/drag-overlay for more info<divclassName="draggable draggable-overlay">{activeItem.name}</div>)}</DragOverlay></DndContext>);functionhandelDragStart(ev: DragStartEvent){const{active} = ev;constactiveId = active.id;constactiveDraggable = draggables.find((draggable)=>draggable.id === activeId);setActiveItem(activeDraggable);}functionhandleDragOver(ev: DragOverEvent){const{active,over} = ev;if(!over)return;//the id of the active draggableconstactiveId = active.id;//The id of the continaer a draggable is dragged over//in our example the overId can either be ROOT, A or BconstoverId = over.idas string;setDraggables((draggables)=>{returndraggables.map((draggable)=>{//if we are dragging a draggable over a containerif(draggable.id === activeId){return{...draggable,//update its containerId to match the overIdcontainerId:overId};}returndraggable;});});}}exportdefaultApp;
Single Container Sortables
A container containing a list of items that can be rearranged by dragging them.
importtype{DragEndEvent}from"@dnd-kit/core";import{closestCenter,DndContext,KeyboardSensor,TouchSensor,MouseSensor,useSensor,useSensors}from"@dnd-kit/core";import{arrayMove,SortableContext,sortableKeyboardCoordinates,verticalListSortingStrategy}from"@dnd-kit/sortable";importReactfrom"react";import{items}from"./items";import{SortableItem}from"./SortableItem";exportdefaultfunctionApp(){const[sortables,setSortables] = React.useState([...items]);//Sensors are an abstraction to detect different input methods in//order to initiate drag operations, respond to movement and end or//cancel the operation. See more here: https://docs.dndkit.com/api-documentation/sensorsconstsensors = useSensors(useSensor(TouchSensor),useSensor(MouseSensor),useSensor(KeyboardSensor,{coordinateGetter:sortableKeyboardCoordinates}));return(<DndContext//collision detection algorithm best suited for sortable interfaces//read more here: https://docs.dndkit.com/api-documentation/context-provider/collision-detection-algorithmscollisionDetection={closestCenter}sensors={sensors}onDragEnd={handleDragEnd}><SortableContext//read more on the SortableContext here https://docs.dndkit.com/presets/sortable/sortable-contextitems={sortables}strategy={verticalListSortingStrategy}><divclassName="app">{sortables.map((sortable)=>(<SortableItemkey={sortable.id}{...sortable}/>))}</div></SortableContext></DndContext>);functionhandleDragEnd(event: DragEndEvent){const{active,over} = event;constactiveId = active.id;constoverId = over?.id;if(activeId && overId && activeId !== overId){setSortables((items)=>{//update items to thier new indexesconstoldIndex = sortables.findIndex((f)=>f.id === activeId);constnewIndex = sortables.findIndex((f)=>f.id === overId);returnarrayMove(items,oldIndex,newIndex);});}}}
Multi-Container Sortables
A list that can be sorted and items that can be dropped into multiple containers.
import{DndContext,KeyboardSensor,TouchSensor,useSensor,useSensors,MouseSensor,closestCorners,DragEndEvent,DragOverEvent,DragStartEvent,DragOverlay}from"@dnd-kit/core";import{arrayMove,horizontalListSortingStrategy,SortableContext,sortableKeyboardCoordinates}from"@dnd-kit/sortable";importReactfrom"react";import{initialItems,Item}from"./items";import{OverlayContainer,SortableContainer,SortableContainerProps}from"./SortableContainer";import{OverlayItem}from"./SortableItem";exportdefaultfunctionApp(){const[sortables,setSortables] = React.useState([...initialItems]);//The state of the active draggable, used to render the drag overlay belowconst[activeItem,setActiveItem] = React.useState<
SortableContainerProps | Item | null
>(null);//Sensors are an abstraction to detect different input methods in//order to initiate drag operations, respond to movement and end or//cancel the operation. See more here: https://docs.dndkit.com/api-documentation/sensorsconstsensors = useSensors(useSensor(TouchSensor),useSensor(MouseSensor),useSensor(KeyboardSensor,{coordinateGetter:sortableKeyboardCoordinates}));constcontainerIds = sortables.map((s)=>s.id);return(<DndContext//collision detection algorithm best suited for sortable interfaces and multiple containers//read more here: https://docs.dndkit.com/api-documentation/context-provider/collision-detection-algorithmscollisionDetection={closestCorners}sensors={sensors}onDragEnd={handleDragEnd}onDragOver={handleDragOver}onDragStart={handleDragStart}><SortableContext//read more on the SortableContext here https://docs.dndkit.com/presets/sortable/sortable-contextitems={sortables}strategy={horizontalListSortingStrategy}><divclassName="app">{sortables.map((s)=>(<SortableContainerkey={s.id}id={s.id}name={s.name}items={s.items}/>))}</div></SortableContext><DragOverlay>{activeItem ? (//Render a drag overlay for either the container or sortable item based on the id of the active item// check here https://docs.dndkit.com/api-documentation/draggable/drag-overlay for more info<>{containerIds.includes(activeItem.id) ? (<OverlayContainer{...(activeItemas SortableContainerProps)}/>) : (<OverlayItem{...(activeItemas Item)}/>)}</>) : null}</DragOverlay></DndContext>);//Returns the id of a container based on the id of any of its child itemsfunctionfindContainer(id?: string){if(id){if(containerIds.includes(id))returnid;constcontainer = sortables?.find((i)=>i.items?.find((l)=>l?.id === id));returncontainer?.id;}}/*Returns true if we are sorting containers
* we will know if we are sorting containers if the id of the active item is a
* container id and it is being dragged over any item in the over container
* or the over container itself
*/functionisSortingContainers({activeId,overId}:{activeId: string;overId: string;}){constisActiveContainer = containerIds.includes(activeId);constisOverContainer =
findContainer(overId) || containerIds.includes(overId);return !!isActiveContainer && !!isOverContainer;}functionhandleDragStart(event: DragStartEvent){const{active} = event;constactiveId = active.idas string;if(containerIds.includes(activeId)){//set the state of active item if we are dragging a containerconstcontainer = sortables.find((i)=>i.id === activeId);if(container)setActiveItem(container);}else{//set the state of active item if we are dragging a container itemconstcontainerId = findContainer(activeId);constcontainer = sortables.find((i)=>i.id === containerId);constitem = container?.items.find((i)=>i.id === activeId);if(item)setActiveItem(item);}}/*In this function we handle when a sortable item is dragged from one container
to another container, to do this we need to know:
- what item is being dragged
- what container it is being dragged from
- what container it is being dragged to
- what index to insert the active item into, in the new container
*/functionhandleDragOver(event: DragOverEvent){const{active,over} = event;if(!active || !over)return;constactiveId = active.idas string;constoverId = over.idas string;//find the container id of the active item and the container id of the item being dragged overconstactiveContainerId = findContainer(activeId);constoverContainerId = findContainer(overId);if(!overContainerId || !activeContainerId)return;//we don't want to sort containers, so we return early if we are sorting containersif(isSortingContainers({activeId,overId}))return;//we only want to update the state if we are dragging over a different containerif(activeContainerId !== overContainerId){constactiveContainer = sortables.find((i)=>i.id === activeContainerId);constoverContainer = sortables.find((i)=>i.id === overContainerId);constactiveItems = activeContainer?.items || [];constactiveIndex = activeItems.findIndex((i)=>i.id === activeId);constoverItems = overContainer?.items || [];constoverIndex = sortables.findIndex((i)=>i.id === overId);letnewIndex: number;if(containerIds.includes(overId)){//if the container is empty, and we drag over it, the overId would be the id of that container and not//the id of any of its items since it is empty so we want to add the item to the end of//the container basically making it the last itemnewIndex = overItems.length + 1;}else{//Get the new index of the item being dragged over if it is a sortable item in the over containerconstisBelowOverItem =
over &&
active.rect.current.translated &&
active.rect.current.translated.top > over.rect.top + over.rect.height;constmodifier = isBelowOverItem ? 1 : 0;newIndex = overIndex >= 0 ? overIndex + modifier : overItems.length + 1;}//Update the stateconstnewItems = sortables.map((item)=>// Remove the active item from the old listitem.id === activeContainerId
? {...item,items:activeItems.filter((item)=>item.id !== active.id)}
: // Add the active item to the new listitem.id === overContainerId
? {...item,items:[...item.items.slice(0,newIndex),activeItems[activeIndex],...overItems.slice(newIndex,item.items.length)]}
: item);setSortables(newItems);}}/*In this function we handle when a sortable item is sorted within its container
or when a sortable container is sorted with other sortable container
*/functionhandleDragEnd(event: DragEndEvent){const{active,over} = event;if(!active || !over)return;constactiveId = active.idas string;constoverId = over.idas string;constactiveContainerId = findContainer(activeId);constoverContainerId = findContainer(overId);if(isSortingContainers({activeId,overId})){if(activeId !== overId){//update sortable containers to their new positionssetSortables((items)=>{constoldIndex = sortables.findIndex((f)=>f.id === activeContainerId);constnewIndex = sortables.findIndex((f)=>f.id === overContainerId);returnarrayMove(items,oldIndex,newIndex);});}}if(activeContainerId === overContainerId){constactiveContainer = sortables.find((i)=>i.id === activeContainerId);constactiveItems = activeContainer?.items || [];constoldIndex = activeItems.findIndex((i)=>i.id === activeId);constnewIndex = activeItems.findIndex((i)=>i.id === overId);//update sortable items to their new positionsconstnewItems = sortables.map((s)=>s.id === activeContainerId
? {...s,items:arrayMove(s.items,oldIndex,newIndex)}
: s);if(oldIndex !== newIndex){setSortables(newItems);}}}}
Closing Thoughts
I hope I managed to demonstrate the power of DndKit. Its strength lies in its flexibility. You can use it to create anything from a simple to-do list to a complex sortable interface.
I hope you enjoyed this post. If you have any questions, you can reach me on Twitter @kxlaa_ and I'll be happy to answer them.