controls

controls

Define which areas can initiate dragging

The controls plugin defines which parts of an element can be used to start dragging. Think modal headers, card handles, or toolbar grips - only specific areas should be draggable while the rest remains interactive.

controls({ allow: ControlFrom.selector('.handle') }); // Only handle can drag
controls({ block: ControlFrom.selector('.button') }); // Buttons can't drag
controls({
  allow: ControlFrom.selector('.header'),
  block: ControlFrom.selector('.close'),
}); // Header drags, close button doesn't

Basic Usage

import { useRef } from 'react';
import { controls, ControlFrom, useDraggable } from '@neodrag/react';

function ControlledDragging() {
  const allowRef = useRef<HTMLDivElement>(null);
  const blockRef = useRef<HTMLDivElement>(null);

  useDraggable(allowRef, [
    controls({ allow: ControlFrom.selector('.header') }),
  ]);
  useDraggable(blockRef, [
    controls({ block: ControlFrom.selector('button') }),
  ]);

  return (
    <div>
      <div ref={allowRef}>
        <div className="header">Drag handle</div>
        <div className="content">Content (not draggable)</div>
      </div>

      <div ref={blockRef}>
        <p>Drag anywhere in this area</p>
        <button>Can't drag from here</button>
      </div>
    </div>
  );
}

ControlFrom Utilities

Selector-Based Controls

import { useRef } from 'react';
import { controls, ControlFrom, useDraggable } from '@neodrag/react';

function SelectorControls() {
  const singleRef = useRef<HTMLDivElement>(null);
  const multiRef = useRef<HTMLDivElement>(null);
  const blockRef = useRef<HTMLDivElement>(null);

  useDraggable(singleRef, [
    controls({ allow: ControlFrom.selector('.handle') }),
  ]);

  useDraggable(multiRef, [
    controls({
      allow: ControlFrom.selector('.handle, .title, .grip'),
    }),
  ]);

  useDraggable(blockRef, [
    controls({
      block: ControlFrom.selector('button, input, select'),
    }),
  ]);

  return (
    <div>
      <div ref={singleRef}>
        <div className="handle">Drag me</div>
        <div>Not draggable</div>
      </div>

      <div ref={multiRef}>
        <div className="title">Title bar</div>
        <div className="handle">Handle</div>
        <div className="grip">⋮⋮</div>
        <div className="content">Content</div>
      </div>

      <div ref={blockRef}>
        <p>Draggable area</p>
        <button>Button (blocked)</button>
        <input placeholder="Input (blocked)" />
      </div>
    </div>
  );
}

Element-Based Controls

import { useRef } from 'react';
import { controls, ControlFrom, useDraggable } from '@neodrag/react';

function ElementControls() {
  const containerRef = useRef<HTMLDivElement>(null);
  const handleRef = useRef<HTMLDivElement>(null);
  const blockContainerRef = useRef<HTMLDivElement>(null);
  const blockRef = useRef<HTMLDivElement>(null);

  useDraggable(containerRef, [
    controls({
      allow: () => ControlFrom.elements([handleRef.current!]),
    }),
  ]);

  useDraggable(blockContainerRef, [
    controls({
      block: () => ControlFrom.elements([blockRef.current!]),
    }),
  ]);

  return (
    <div>
      <div ref={containerRef}>
        <div ref={handleRef}>Specific handle element</div>
        <div>Not draggable</div>
      </div>

      <div ref={blockContainerRef}>
        <p>Draggable area</p>
        <div ref={blockRef}>Blocked element</div>
      </div>
    </div>
  );
}

Advanced Control Logic

Priority Handling

import { useRef } from 'react';
import { controls, ControlFrom, useDraggable } from '@neodrag/react';

function PriorityControls() {
  const allowWinsRef = useRef<HTMLDivElement>(null);
  const blockWinsRef = useRef<HTMLDivElement>(null);

  useDraggable(allowWinsRef, [
    controls({
      allow: ControlFrom.selector('.drag-zone'),
      block: ControlFrom.selector('.no-drag'),
      priority: 'allow',
    }),
  ]);

  useDraggable(blockWinsRef, [
    controls({
      allow: ControlFrom.selector('.drag-zone'),
      block: ControlFrom.selector('.no-drag'),
      priority: 'block',
    }),
  ]);

  return (
    <div>
      <div ref={allowWinsRef}>
        <div className="drag-zone">
          Drag zone
          <div className="no-drag">Allow wins here</div>
        </div>
      </div>

      <div ref={blockWinsRef}>
        <div className="drag-zone">
          Drag zone
          <div className="no-drag">Block wins here</div>
        </div>
      </div>
    </div>
  );
}

Real-World Examples

import { useRef } from 'react';
import {
  useDraggable,
  controls,
  ControlFrom,
  bounds,
  BoundsFrom,
} from '@neodrag/react';

function DraggableModal() {
  const modalRef = useRef<HTMLDivElement>(null);

  useDraggable(modalRef, [
    controls({
      allow: ControlFrom.selector('.modal-header'),
      block: ControlFrom.selector('.modal-close, .modal-body button'),
    }),
    bounds(BoundsFrom.viewport()),
  ]);

  return (
    <div ref={modalRef}>
      <div className="modal-header">
        <h3>Modal Title</h3>
        <button className="modal-close">×</button>
      </div>
      <div className="modal-body">
        <p>Modal content</p>
        <button>Action Button</button>
      </div>
    </div>
  );
}

How It Works

The controls plugin manages drag initiation permissions:

  1. Finds control zones on setup by querying the DOM
  2. Checks click position in shouldStart hook
  3. Determines permission based on which zones contain the click point
  4. Returns boolean to allow/prevent drag start

Zone priority logic:

  • If allow zones exist and click is outside all of them → block
  • If click is in both allow and block zones → use priority option
  • Smaller (nested) zones take precedence over larger ones

API Reference

function controls(options?: {
  allow?: ReturnType<
    typeof ControlFrom.selector | typeof ControlFrom.elements
  >;
  block?: ReturnType<
    typeof ControlFrom.selector | typeof ControlFrom.elements
  >;
  priority?: 'allow' | 'block';
}): Plugin;

Options:

  • allow - Control zones that can initiate dragging
  • block - Control zones that cannot initiate dragging
  • priority - Which wins when zones overlap (‘allow’ | ‘block’)

ControlFrom utilities:

  • ControlFrom.selector(cssSelector) - Elements matching CSS selector
  • ControlFrom.elements(nodeList) - Specific DOM elements

Returns: A plugin object for use with draggable.