CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-react-dnd

Drag and Drop for React applications with hooks-based API and TypeScript support

Pending

Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

Overview
Eval results
Files

drag-layer.mddocs/

Drag Layer

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.

Capabilities

useDragLayer Hook

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>
  );
}

Drag Layer Monitor

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;
}

Common Patterns

Multi-type Custom Previews

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>
  );
}

Responsive Drag Feedback

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>
  );
}

Drag State Indicator

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>
  );
}

Drag Constraints Visualization

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>
  );
}

Global Drag Layer Portal

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

docs

context-provider.md

drag-layer.md

drag-sources.md

drop-targets.md

index.md

utilities.md

tile.json