Drag and Drop for React applications with hooks-based API and TypeScript support
—
Quality
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
The useDragLayer hook enables access to global drag state for creating custom drag previews and drag-aware components that respond to drag operations anywhere in the application.
Hook for accessing global drag state and creating custom drag layer components.
/**
* Hook for accessing drag layer state for custom drag previews
* @param collect - Function to collect properties from the drag layer monitor
* @returns Collected properties from the monitor
*/
function useDragLayer<CollectedProps, DragObject = any>(
collect: (monitor: DragLayerMonitor<DragObject>) => CollectedProps
): CollectedProps;Basic Usage:
import React from "react";
import { useDragLayer } from "react-dnd";
function CustomDragLayer() {
const { isDragging, itemType, item, currentOffset } = useDragLayer((monitor) => ({
isDragging: monitor.isDragging(),
itemType: monitor.getItemType(),
item: monitor.getItem(),
currentOffset: monitor.getClientOffset(),
}));
if (!isDragging || !currentOffset) {
return null;
}
return (
<div
style={{
position: "fixed",
pointerEvents: "none",
zIndex: 100,
left: currentOffset.x,
top: currentOffset.y,
transform: "translate(-50%, -50%)",
}}
>
<CustomPreview itemType={itemType} item={item} />
</div>
);
}
function CustomPreview({ itemType, item }) {
switch (itemType) {
case "card":
return <div className="card-preview">{item.name}</div>;
case "file":
return <div className="file-preview">📄 {item.filename}</div>;
default:
return <div className="default-preview">Dragging...</div>;
}
}Advanced Drag Layer with Animations:
import React, { useState, useEffect } from "react";
import { useDragLayer } from "react-dnd";
function AnimatedDragLayer() {
const [trail, setTrail] = useState([]);
const {
isDragging,
itemType,
item,
currentOffset,
initialOffset,
differenceFromInitialOffset,
} = useDragLayer((monitor) => ({
isDragging: monitor.isDragging(),
itemType: monitor.getItemType(),
item: monitor.getItem(),
currentOffset: monitor.getClientOffset(),
initialOffset: monitor.getInitialClientOffset(),
differenceFromInitialOffset: monitor.getDifferenceFromInitialOffset(),
}));
// Create trail effect
useEffect(() => {
if (isDragging && currentOffset) {
setTrail(prev => [
...prev.slice(-10), // Keep last 10 positions
{ x: currentOffset.x, y: currentOffset.y, timestamp: Date.now() }
]);
} else {
setTrail([]);
}
}, [isDragging, currentOffset]);
if (!isDragging) {
return null;
}
const layerStyles = {
position: "fixed" as const,
pointerEvents: "none" as const,
zIndex: 100,
left: 0,
top: 0,
width: "100%",
height: "100%",
};
return (
<div style={layerStyles}>
{/* Trail effect */}
{trail.map((point, index) => (
<div
key={point.timestamp}
style={{
position: "absolute",
left: point.x,
top: point.y,
width: 4,
height: 4,
backgroundColor: "rgba(0, 100, 255, " + (index / trail.length) + ")",
borderRadius: "50%",
transform: "translate(-50%, -50%)",
}}
/>
))}
{/* Main preview */}
{currentOffset && (
<div
style={{
position: "absolute",
left: currentOffset.x,
top: currentOffset.y,
transform: "translate(-50%, -50%) rotate(" +
(differenceFromInitialOffset ? differenceFromInitialOffset.x * 0.1 : 0) + "deg)",
transition: "transform 0.1s ease-out",
}}
>
<DragPreview itemType={itemType} item={item} />
</div>
)}
</div>
);
}Monitor interface providing global drag state information.
interface DragLayerMonitor<DragObject = unknown> {
/** Returns true if any drag operation is in progress */
isDragging(): boolean;
/** Returns the type of item being dragged */
getItemType(): Identifier | null;
/** Returns the dragged item data */
getItem<T = DragObject>(): T;
/** Returns initial pointer coordinates when drag started */
getInitialClientOffset(): XYCoord | null;
/** Returns initial drag source coordinates */
getInitialSourceClientOffset(): XYCoord | null;
/** Returns current pointer coordinates */
getClientOffset(): XYCoord | null;
/** Returns pointer movement since drag start */
getDifferenceFromInitialOffset(): XYCoord | null;
/** Returns projected source coordinates */
getSourceClientOffset(): XYCoord | null;
}function TypeAwareDragLayer() {
const { isDragging, itemType, item, currentOffset } = useDragLayer((monitor) => ({
isDragging: monitor.isDragging(),
itemType: monitor.getItemType(),
item: monitor.getItem(),
currentOffset: monitor.getClientOffset(),
}));
const renderPreview = () => {
switch (itemType) {
case "card":
return (
<div className="card-drag-preview">
<h4>{item.title}</h4>
<p>{item.content}</p>
</div>
);
case "file":
return (
<div className="file-drag-preview">
<span className="file-icon">{getFileIcon(item.type)}</span>
<span className="file-name">{item.name}</span>
<span className="file-size">{formatSize(item.size)}</span>
</div>
);
case "list-item":
return (
<div className="list-item-preview">
<div className="item-count">{item.items?.length || 1} item(s)</div>
<div className="item-title">{item.title}</div>
</div>
);
default:
return <div className="default-preview">Dragging {itemType}</div>;
}
};
if (!isDragging || !currentOffset) {
return null;
}
return (
<div
style={{
position: "fixed",
pointerEvents: "none",
zIndex: 1000,
left: currentOffset.x,
top: currentOffset.y,
transform: "translate(-50%, -50%)",
}}
>
{renderPreview()}
</div>
);
}function ResponsiveDragLayer() {
const {
isDragging,
item,
currentOffset,
differenceFromInitialOffset,
} = useDragLayer((monitor) => ({
isDragging: monitor.isDragging(),
item: monitor.getItem(),
currentOffset: monitor.getClientOffset(),
differenceFromInitialOffset: monitor.getDifferenceFromInitialOffset(),
}));
if (!isDragging || !currentOffset || !differenceFromInitialOffset) {
return null;
}
// Calculate drag velocity and direction
const velocity = Math.sqrt(
Math.pow(differenceFromInitialOffset.x, 2) +
Math.pow(differenceFromInitialOffset.y, 2)
);
const scale = Math.min(1.2, 1 + velocity * 0.001);
const rotation = differenceFromInitialOffset.x * 0.05;
return (
<div
style={{
position: "fixed",
pointerEvents: "none",
zIndex: 100,
left: currentOffset.x,
top: currentOffset.y,
transform: `translate(-50%, -50%) scale(${scale}) rotate(${rotation}deg)`,
transition: "transform 0.1s ease-out",
}}
>
<div className="responsive-preview">
{item.name}
<div className="velocity-indicator" style={{ opacity: velocity * 0.01 }}>
Fast!
</div>
</div>
</div>
);
}function DragStateIndicator() {
const { isDragging, itemType, currentOffset } = useDragLayer((monitor) => ({
isDragging: monitor.isDragging(),
itemType: monitor.getItemType(),
currentOffset: monitor.getClientOffset(),
}));
return (
<div className="drag-state-indicator">
<div className={`status ${isDragging ? "active" : "inactive"}`}>
{isDragging ? `Dragging ${itemType}` : "No active drag"}
</div>
{isDragging && currentOffset && (
<div className="coordinates">
Position: {Math.round(currentOffset.x)}, {Math.round(currentOffset.y)}
</div>
)}
</div>
);
}function ConstrainedDragLayer({ bounds }) {
const { isDragging, currentOffset, item } = useDragLayer((monitor) => ({
isDragging: monitor.isDragging(),
currentOffset: monitor.getClientOffset(),
item: monitor.getItem(),
}));
if (!isDragging || !currentOffset) {
return null;
}
// Check if drag is within allowed bounds
const isWithinBounds = bounds &&
currentOffset.x >= bounds.left &&
currentOffset.x <= bounds.right &&
currentOffset.y >= bounds.top &&
currentOffset.y <= bounds.bottom;
return (
<div
style={{
position: "fixed",
pointerEvents: "none",
zIndex: 100,
left: currentOffset.x,
top: currentOffset.y,
transform: "translate(-50%, -50%)",
}}
>
<div
className={`constrained-preview ${isWithinBounds ? "valid" : "invalid"}`}
style={{
border: isWithinBounds ? "2px solid green" : "2px solid red",
backgroundColor: isWithinBounds ? "rgba(0,255,0,0.1)" : "rgba(255,0,0,0.1)",
}}
>
{item.name}
{!isWithinBounds && <div className="warning">⚠️ Outside bounds</div>}
</div>
</div>
);
}import React from "react";
import { createPortal } from "react-dom";
import { useDragLayer } from "react-dnd";
function GlobalDragLayerPortal() {
const { isDragging, itemType, item, currentOffset } = useDragLayer((monitor) => ({
isDragging: monitor.isDragging(),
itemType: monitor.getItemType(),
item: monitor.getItem(),
currentOffset: monitor.getClientOffset(),
}));
if (!isDragging || !currentOffset) {
return null;
}
const dragLayer = (
<div
style={{
position: "fixed",
pointerEvents: "none",
zIndex: 9999,
left: currentOffset.x,
top: currentOffset.y,
transform: "translate(-50%, -50%)",
}}
>
<GlobalDragPreview itemType={itemType} item={item} />
</div>
);
// Render to body to ensure it's always on top
return createPortal(dragLayer, document.body);
}
function GlobalDragPreview({ itemType, item }) {
return (
<div className={`global-drag-preview ${itemType}`}>
<div className="preview-content">
{item.name || item.title || "Dragging..."}
</div>
<div className="preview-shadow" />
</div>
);
}Install with Tessl CLI
npx tessl i tessl/npm-react-dnd