Skip to main content

Usage

Makes an element draggable and keeps its absolute position in sync with pointer/touch movement. Supports drag handles, axis locking, viewport constraints, drag lifecycle callbacks, and imperative repositioning.
import { useFloatingWindow } from '@kuzenbo/hooks';

function Demo() {
  const { ref, isDragging } = useFloatingWindow();

  return (
    <div
      ref={ref}
      style={{
        position: 'absolute',
        padding: '1rem',
        background: 'white',
        border: '1px solid #ccc',
        cursor: isDragging ? 'grabbing' : 'grab',
      }}
    >
      Drag me anywhere
    </div>
  );
}

Constrain to viewport

Keep the element within viewport boundaries:
import { useFloatingWindow } from '@kuzenbo/hooks';

function Demo() {
  const { ref } = useFloatingWindow({
    constrainToViewport: true,
    constrainOffset: 10, // 10px from edges
  });

  return (
    <div
      ref={ref}
      style={{
        position: 'absolute',
        padding: '1rem',
        background: 'white',
        border: '1px solid #ccc',
      }}
    >
      Can't leave the viewport
    </div>
  );
}

Drag handle

Only allow dragging from a specific handle element:
import { useFloatingWindow } from '@kuzenbo/hooks';

function Demo() {
  const { ref } = useFloatingWindow({
    dragHandleSelector: '.drag-handle',
  });

  return (
    <div
      ref={ref}
      style={{
        position: 'absolute',
        background: 'white',
        border: '1px solid #ccc',
      }}
    >
      <div className="drag-handle" style={{ padding: '0.5rem', background: '#eee', cursor: 'grab' }}>
        Drag here
      </div>
      <div style={{ padding: '1rem' }}>
        <p>Content area (not draggable)</p>
        <button>Click me</button>
      </div>
    </div>
  );
}

Axis locking

Restrict movement to a single axis:
import { useFloatingWindow } from '@kuzenbo/hooks';

function Demo() {
  const horizontal = useFloatingWindow({ axis: 'x' });
  const vertical = useFloatingWindow({ axis: 'y' });

  return (
    <>
      <div
        ref={horizontal.ref}
        style={{
          position: 'absolute',
          top: 100,
          left: 100,
          padding: '1rem',
          background: 'lightblue',
        }}
      >
        Horizontal only
      </div>
      
      <div
        ref={vertical.ref}
        style={{
          position: 'absolute',
          top: 200,
          left: 100,
          padding: '1rem',
          background: 'lightcoral',
        }}
      >
        Vertical only
      </div>
    </>
  );
}

Initial position

Set the initial position:
import { useFloatingWindow } from '@kuzenbo/hooks';

function Demo() {
  const { ref } = useFloatingWindow({
    initialPosition: { top: 50, left: 100 },
  });

  return (
    <div
      ref={ref}
      style={{
        position: 'absolute',
        padding: '1rem',
        background: 'white',
        border: '1px solid #ccc',
      }}
    >
      Positioned at top: 50, left: 100
    </div>
  );
}

Imperative positioning

Programmatically move the element:
import { useFloatingWindow } from '@kuzenbo/hooks';

function Demo() {
  const { ref, setPosition } = useFloatingWindow();

  return (
    <>
      <div style={{ marginBottom: '1rem' }}>
        <button onClick={() => setPosition({ top: 100, left: 100 })}>
          Move to (100, 100)
        </button>
        <button onClick={() => setPosition({ bottom: 50, right: 50 })}>
          Move to bottom-right
        </button>
      </div>
      
      <div
        ref={ref}
        style={{
          position: 'absolute',
          padding: '1rem',
          background: 'white',
          border: '1px solid #ccc',
        }}
      >
        Draggable window
      </div>
    </>
  );
}

Drag callbacks

Listen to drag lifecycle events:
import { useFloatingWindow } from '@kuzenbo/hooks';
import { useState } from 'react';

function Demo() {
  const [position, setPosition] = useState({ x: 0, y: 0 });
  
  const { ref, isDragging } = useFloatingWindow({
    onDragStart: () => console.log('Drag started'),
    onDragEnd: () => console.log('Drag ended'),
    onPositionChange: (pos) => setPosition(pos),
  });

  return (
    <div
      ref={ref}
      style={{
        position: 'absolute',
        padding: '1rem',
        background: 'white',
        border: '1px solid #ccc',
      }}
    >
      <p>Dragging: {isDragging ? 'Yes' : 'No'}</p>
      <p>Position: ({Math.round(position.x)}, {Math.round(position.y)})</p>
    </div>
  );
}

Definition

interface FloatingWindowPosition {
  x: number;
  y: number;
}

interface FloatingWindowPositionConfig {
  top?: number;
  left?: number;
  right?: number;
  bottom?: number;
}

interface UseFloatingWindowOptions {
  enabled?: boolean;
  constrainToViewport?: boolean;
  constrainOffset?: number;
  dragHandleSelector?: string;
  excludeDragHandleSelector?: string;
  axis?: 'x' | 'y';
  initialPosition?: FloatingWindowPositionConfig;
  onPositionChange?: (pos: FloatingWindowPosition) => void;
  onDragStart?: () => void;
  onDragEnd?: () => void;
}

interface UseFloatingWindowReturnValue<T extends HTMLElement> {
  ref: RefCallback<T | null>;
  setPosition: (position: FloatingWindowPositionConfig) => void;
  isDragging: boolean;
}

function useFloatingWindow<T extends HTMLElement>(
  options?: UseFloatingWindowOptions
): UseFloatingWindowReturnValue<T>

Parameters

options
UseFloatingWindowOptions
enabled
boolean
default:true
If false, the element cannot be dragged
constrainToViewport
boolean
default:false
If true, the element can only move within the current viewport boundaries
constrainOffset
number
default:0
The offset from the viewport edges when constraining the element. Requires constrainToViewport: true
dragHandleSelector
string
Selector of an element that should be used to drag floating window. If not specified, the entire root element is used as a drag target
excludeDragHandleSelector
string
Selector of an element within dragHandleSelector that should be excluded from the drag event
axis
'x' | 'y'
If set, restricts movement to the specified axis
initialPosition
FloatingWindowPositionConfig
Initial position. If not set, calculated from element styles
onPositionChange
(pos: FloatingWindowPosition) => void
Called when the element position changes
onDragStart
() => void
Called when the drag starts
onDragEnd
() => void
Called when the drag stops

Returns

ref
RefCallback<T | null>
Ref to attach to the draggable element
setPosition
(position: FloatingWindowPositionConfig) => void
Function to imperatively set the element position
isDragging
boolean
true if the element is currently being dragged

Build docs developers (and LLMs) love