bounds

bounds

Keep draggable elements within specified boundaries

The bounds plugin keeps draggable elements within specified boundaries. Whether it’s staying inside a parent container, the viewport, or custom regions - bounds ensures your elements can’t escape their designated areas.

bounds(BoundsFrom.viewport()); // Stay in browser window
bounds(BoundsFrom.parent()); // Stay in parent element
bounds(BoundsFrom.element(el)); // Stay in specific element

Basic Usage

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

function BoundedElements() {
  const viewportRef = useRef<HTMLDivElement>(null);
  const parentRef = useRef<HTMLDivElement>(null);

  useDraggable(viewportRef, [bounds(BoundsFrom.viewport())]);
  useDraggable(parentRef, [bounds(BoundsFrom.parent())]);

  return (
    <div>
      <div ref={viewportRef}>Can't leave the screen</div>
      <div className="container">
        <div ref={parentRef}>Contained child</div>
      </div>
    </div>
  );
}

BoundsFrom Utilities

Viewport Boundaries

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

function ViewportBounds() {
  const basicRef = useRef<HTMLDivElement>(null);
  const paddedRef = useRef<HTMLDivElement>(null);

  useDraggable(basicRef, [bounds(BoundsFrom.viewport())]);
  useDraggable(paddedRef, [
    bounds(
      BoundsFrom.viewport({
        top: 20,
        left: 20,
        right: 20,
        bottom: 20,
      }),
    ),
  ]);

  return (
    <div>
      <div ref={basicRef}>Stay in window</div>
      <div ref={paddedRef}>20px padding from edges</div>
    </div>
  );
}

Element-Specific Boundaries

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

function ElementBounds() {
  const containerRef = useRef<HTMLDivElement>(null);
  const boundedRef = useRef<HTMLDivElement>(null);
  const selectorRef = useRef<HTMLDivElement>(null);

  useDraggable(boundedRef, [
    bounds(() => BoundsFrom.element(containerRef.current!)),
  ]);
  useDraggable(selectorRef, [
    bounds(BoundsFrom.selector('#drop-zone')),
  ]);

  return (
    <div>
      <div ref={containerRef} className="boundary-container">
        <div ref={boundedRef}>Bound to container</div>
      </div>

      <div className="zone" id="drop-zone">
        <div ref={selectorRef}>Bound by selector</div>
      </div>
    </div>
  );
}

Custom Boundary Functions

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

function CustomBounds() {
  const circularRef = useRef<HTMLDivElement>(null);
  const timeRef = useRef<HTMLDivElement>(null);

  // Custom circular boundary
  const circularBounds = () => {
    const centerX = window.innerWidth / 2;
    const centerY = window.innerHeight / 2;
    const radius = 200;

    return [
      [centerX - radius, centerY - radius],
      [centerX + radius, centerY + radius],
    ] as [[number, number], [number, number]];
  };

  // Dynamic boundary based on time
  const timeBounds = () => {
    const time = Date.now() / 1000;
    const padding = Math.sin(time) * 50 + 100;

    return [
      [padding, padding],
      [window.innerWidth - padding, window.innerHeight - padding],
    ] as [[number, number], [number, number]];
  };

  useDraggable(circularRef, [bounds(circularBounds)]);
  useDraggable(timeRef, [bounds(timeBounds)]);

  return (
    <div>
      <div ref={circularRef}>Circular boundary</div>
      <div ref={timeRef}>Animated boundary</div>
    </div>
  );
}

Dynamic Boundaries

For boundaries that change during dragging, control when they’re recalculated:

bounds(boundaryFunction, (ctx) => {
  // Recompute on every drag event
  return ctx.hook === 'drag';
});

bounds(boundaryFunction, (ctx) => {
  // Only recompute at start and end
  return ctx.hook === 'dragStart' || ctx.hook === 'dragEnd';
});

How It Works

The bounds plugin calculates the allowed movement area and clamps the proposed position:

  1. Boundary calculation - Runs the boundary function to get [[x1, y1], [x2, y2]]
  2. Element positioning - Considers the element’s size and current position
  3. Movement clamping - Limits proposed.x and proposed.y to stay within bounds

The boundary function is called:

  • On drag start (always)
  • During drag (if shouldRecompute returns true)
  • On drag end (if shouldRecompute returns true)

API Reference

function bounds(
  boundsFn: () => [[number, number], [number, number]],
  shouldRecompute?: (ctx: {
    hook: 'dragStart' | 'drag' | 'dragEnd';
  }) => boolean,
): Plugin;

Parameters:

  • boundsFn - Function returning [[x1, y1], [x2, y2]] coordinates
  • shouldRecompute - When to recalculate boundaries (default: drag start only)

BoundsFrom utilities:

  • BoundsFrom.viewport(padding?) - Browser viewport
  • BoundsFrom.parent(padding?) - Parent element
  • BoundsFrom.element(el, padding?) - Specific element
  • BoundsFrom.selector(selector, padding?, root?) - Element by selector

Returns: A plugin object for use with draggable.