LogoRobo.js

Draggables

Synchronized drag-and-drop with locking, interpolation, and bounds.

Create draggable elements that sync position across clients in real-time. The useSyncDrag hook handles gesture detection, locking, interpolation, and bounds. The SyncDraggable component wraps it in a declarative API.

SyncDraggable component

The easiest way to create synchronized draggable elements.

import { SyncDraggable } from '@robojs/sync'

function GameBoard() {
  return (
    <div style={{ position: 'relative', width: '100%', height: '100%' }}>
      <SyncDraggable
        id={['piece', '1']}
        initial={{ x: 0.5, y: 0.5 }}
        interpolate={{ x: 0.2, y: 0.2 }}
        bounds={{ minX: 0.05, maxX: 0.95, minY: 0.05, maxY: 0.95 }}
      >
        <div className="game-piece">Drag me</div>
      </SyncDraggable>
    </div>
  )
}
import { SyncDraggable } from '@robojs/sync'

function GameBoard() {
  return (
    <div style={{ position: 'relative', width: '100%', height: '100%' }}>
      <SyncDraggable
        id={['piece', '1']}
        initial={{ x: 0.5, y: 0.5 }}
        interpolate={{ x: 0.2, y: 0.2 }}
        bounds={{ minX: 0.05, maxX: 0.95, minY: 0.05, maxY: 0.95 }}
      >
        <div className="game-piece">Drag me</div>
      </SyncDraggable>
    </div>
  )
}

Props

PropTypeDefaultDescription
id(string | null)[](required)Key suffix for state sync
initialT(required)Initial state (must include x, y)
interpolate{ [field]: number }undefinedLerp factor per field (0-1)
throttlenumber16Milliseconds between updates
bounds{ minX?, maxX?, minY?, maxY? }undefinedPosition constraints
normalizebooleantrueUse 0-1 viewport coordinates
childrenReactNode | (props) => ReactNode(required)Content or render function
onDragStart(state: T) => voidundefinedCalled when drag starts
onDrag(state: T) => voidundefinedCalled on each drag update
onDragEnd(state: T) => voidundefinedCalled when drag ends
onStateChange(state: T, prev: T) => voidundefinedCalled on any state change
styleCSSPropertiesundefinedWrapper styles
classNamestringundefinedWrapper class
asElementType | null'div'Wrapper element

Render props

<SyncDraggable
  id={['card', cardId]}
  initial={{ x: 0.1, y: 0.1 }}
  interpolate={{ x: 0.15, y: 0.15 }}
  onDragEnd={(s) => console.log('Dropped at', s.x, s.y)}
>
  {({ state, isDragging, canInteract }) => (
    <div
      style={{
        cursor: isDragging ? 'grabbing' : canInteract ? 'grab' : 'not-allowed',
        opacity: isDragging ? 0.8 : 1
      }}
    >
      Card ({state.x.toFixed(2)}, {state.y.toFixed(2)})
    </div>
  )}
</SyncDraggable>
<SyncDraggable
  id={['card', cardId]}
  initial={{ x: 0.1, y: 0.1 }}
  interpolate={{ x: 0.15, y: 0.15 }}
  onDragEnd={(s) => console.log('Dropped at', s.x, s.y)}
>
  {({ state, isDragging, canInteract }) => (
    <div
      style={{
        cursor: isDragging ? 'grabbing' : canInteract ? 'grab' : 'not-allowed',
        opacity: isDragging ? 0.8 : 1
      }}
    >
      Card ({state.x.toFixed(2)}, {state.y.toFixed(2)})
    </div>
  )}
</SyncDraggable>

SyncDraggableRenderProps

FieldTypeDescription
stateTCurrent position and data
isDraggingbooleanYou are dragging
isBeingDraggedbooleanAnyone is dragging
canInteractbooleanNot locked by someone else

useSyncDrag hook

For full control without a wrapper component.

Signature

function useSyncDrag<T extends DragState, ClientData = unknown>(
  key: (string | null)[],
  initialState: T,
  options?: DragOptions<T>
): DragResult<T, ClientData>
function useSyncDrag(
  key: (string | null)[],
  initialState: T,
  options?: DragOptions<T>
): DragResult<T, ClientData>

Options

OptionTypeDefaultDescription
interpolate{ [field]: number }undefinedLerp factors for smooth remote updates
throttlenumber16Milliseconds between network updates
bounds{ minX?, maxX?, minY?, maxY? }undefinedPosition constraints
normalizebooleantrueUse 0-1 viewport coordinates
lockOnDragbooleantrueAuto-lock when dragging starts

Return value

FieldTypeDescription
stateTCurrent state (interpolated for remote updates)
setState(update) => voidUpdate state
isDraggingbooleanYou are currently dragging
isBeingDraggedbooleanAnyone is dragging (locked)
canInteractbooleanNot locked by someone else
dragHandlers{ onMouseDown, onTouchStart }Spread onto draggable element
lockLockContextLock context for advanced usage
contextSyncContextSync context

Usage example

import { useSyncDrag } from '@robojs/sync'

function DraggableBall({ id }: { id: string }) {
  const { state, isDragging, canInteract, dragHandlers } = useSyncDrag(
    ['ball', id],
    { x: 0.5, y: 0.5 },
    {
      interpolate: { x: 0.2, y: 0.2 },
      bounds: { minX: 0.05, maxX: 0.95, minY: 0.05, maxY: 0.95 }
    }
  )

  return (
    <div
      {...dragHandlers}
      style={{
        position: 'absolute',
        left: `${state.x * 100}%`,
        top: `${state.y * 100}%`,
        cursor: isDragging ? 'grabbing' : canInteract ? 'grab' : 'not-allowed'
      }}
    >
      Ball
    </div>
  )
}
import { useSyncDrag } from '@robojs/sync'

function DraggableBall({ id }) {
  const { state, isDragging, canInteract, dragHandlers } = useSyncDrag(
    ['ball', id],
    { x: 0.5, y: 0.5 },
    {
      interpolate: { x: 0.2, y: 0.2 },
      bounds: { minX: 0.05, maxX: 0.95, minY: 0.05, maxY: 0.95 }
    }
  )

  return (
    <div
      {...dragHandlers}
      style={{
        position: 'absolute',
        left: `${state.x * 100}%`,
        top: `${state.y * 100}%`,
        cursor: isDragging ? 'grabbing' : canInteract ? 'grab' : 'not-allowed'
      }}
    >
      Ball
    </div>
  )
}

How locking works

  • When lockOnDrag is true (default for both hook and component), the element is automatically locked when the user starts dragging and unlocked when they stop.
  • While locked, other clients see the element as "not-allowed" and can't interact with it.
  • The lock holder sees instant local updates. Other clients see interpolated movement.
  • If the lock holder disconnects, the lock is released automatically via the server's connection cleanup.

Interpolation

Lerp factors control how smoothly remote updates are applied. Values range from 0 to 1:

  • 0.1 = very smooth, slower convergence
  • 0.3 = balanced
  • 1.0 = instant (no interpolation)

The interpolation loop uses requestAnimationFrame and stops when all fields are within 0.001 of their target.

Touch support

Both useSyncDrag and SyncDraggable handle touch events automatically. The dragHandlers object includes onTouchStart, and global touchmove/touchend listeners are registered during a drag.

Next steps

On this page