React
DragSelect has official React bindings: @dragselect/react.
npm i dragselect @dragselect/react
1. Wrap your component tree
Everything inside a DragSelectProvider shares one DragSelect instance. It takes any of the settings. They are read once on mount; to change them later, call useDragSelect()?.setSettings({...}).
import { DragSelectProvider } from "@dragselect/react";
export const Gallery = ({ photos }) => (
<DragSelectProvider settings={{ selectedClass: "is-selected" }}>
{photos.map((photo) => (
<Photo key={photo.id} photo={photo} />
))}
<Toolbar />
</DragSelectProvider>
);
2. Make components selectable
useSelectable returns a ref. It registers the element while the component is mounted and removes it on unmount. Put whatever you need to identify the item on the element itself, e.g. a data-id attribute.
import { useSelectable } from "@dragselect/react";
const Photo = ({ photo }) => {
const ref = useSelectable();
return <img ref={ref} src={photo.url} data-id={photo.id} />;
};
Selected elements get the ds-selected class (or your selectedClass), so style them with CSS. Selecting does not re-render your items.
3. Read the selection
useSelection returns the selected elements and re-renders the component that calls it whenever the selection changes:
import { useSelection } from "@dragselect/react";
const Toolbar = () => {
const selectedIds = useSelection<HTMLImageElement>().map((el) => el.dataset.id);
return <span>{selectedIds.length} selected</span>;
};
Events
useDragSelectEvent subscribes to any event and unsubscribes on unmount. It always calls the latest callback, so you don't need useCallback.
import { useDragSelectEvent } from "@dragselect/react";
useDragSelectEvent("DS:end", ({ items, isDragging }) => {
console.log(isDragging ? "moved" : "selected", items);
});
Area and drop zones
import {
useDragSelectArea,
useDragSelectEvent,
useDropZone,
useSelection,
} from "@dragselect/react";
const Board = ({ onDelete, children }) => {
const areaRef = useDragSelectArea(); // only select inside this element
const trashRef = useDropZone("trash");
const selectedIds = useSelection().map((el) => el.dataset.id);
useDragSelectEvent("DS:end", ({ isDragging, dropTarget }) => {
if (isDragging && dropTarget?.id === "trash") onDelete(selectedIds);
});
return (
<div ref={areaRef}>
{children}
<div ref={trashRef}>🗑 Trash</div>
</div>
);
};
Drop zones
useDropZone(id) registers a drop zone. The id is how DragSelect names the zone: it comes back as dropTarget.id in events, works with ds.getItemsDroppedByZoneId(id), and suffixes the classes items get (e.g. ds-dropped-target-archive).
import { useDragSelectEvent, useDropZone } from "@dragselect/react";
const Folder = ({ id, name, onMove }) => {
const ref = useDropZone(id);
// `items` is everything that was dragged: one element or the whole selection
useDragSelectEvent("DS:end", ({ items, isDragging, dropTarget }) => {
if (!isDragging || dropTarget?.id !== id) return;
onMove(items.map((el) => el.dataset.id));
});
return <div ref={ref}>📁 {name}</div>;
};
// used as:
<Folder id="archive" name="Archive" onMove={(ids) => archive(ids)} />;
/* the zone while a drag is in progress */
.ds-dropzone-ready { outline: 2px dashed; }
/* items that were dropped into the "archive" zone */
.ds-dropped-target-archive { opacity: 0.5; }
Anything else
useDragSelect() returns the underlying instance, so every method is still available:
import { useDragSelect } from "@dragselect/react";
const ClearButton = () => {
const ds = useDragSelect(); // undefined on the first render
return <button onClick={() => ds?.clearSelection()}>Clear</button>;
};
API overview
| Export | Returns | Purpose |
|---|---|---|
<DragSelectProvider settings?> | Creates and tears down the instance. | |
useSelectable() | ref | Registers a selectable element. |
useSelection<E>() | E[] | The selected elements. |
useDragSelectEvent(name, cb) | Subscribes to an event. | |
useDragSelectArea() | ref | Sets the drag area. |
useDropZone(id) | ref | Registers a drop zone. |
useDragSelect() | DragSelect \| undefined | The raw instance. |
Works with React 18+, StrictMode, SSR, and the Next.js App Router. The bundle is marked 'use client'.
You can see it in use on this site's demo window.