Interact.js Resizable Panel — Draggable + Resizable Snippet

Interact.js Resizable Panel · Layouts · Plain HTML, CSS & JS · Live preview

What's included

Features

Data-attribute position tracking
Position lives in data-x/data-y, avoiding getBoundingClientRect reads on every move event.
Header-scoped dragging
allowFrom restricts drag initiation to the title bar so it never conflicts with resize handles.
All-edge resizing
edges: { left, right, top, bottom } enables resize from any side and both corners implicitly.
Correct top/left origin shift
event.deltaRect.left/top is added onto tracked position so resizing from those edges repositions correctly.
Size clamping via modifier
restrictSize enforces min/max dimensions before the move listener even runs.
Transform-only movement
Both drag and resize apply position via CSS transform, never left/top, for compositor-friendly motion.
Live dimension readout
A small label shows the current pixel width and height as you resize.
Touch-ready
touch-action: none on the panel lets the same interactions work with pointer/touch input.

About this UI Snippet

Interact.js Resizable Panel — The data-x/data-y Pattern Explained

Screenshot of the Interact.js Resizable Panel snippet rendered live

Making an element draggable is easy with raw pointer events. Making it *also* resizable from any edge, without the drag and resize logic corrupting each other's sense of "where the element currently is," is the part that trips people up. interact.js solves this with a specific idiom worth understanding on its own: tracking position in data-x/data-y attributes rather than reading getBoundingClientRect() every event.

Why not just read the DOM each move event?

A tempting approach is: on every move event, call target.getBoundingClientRect(), add the delta, and write the new position. The problem is that getBoundingClientRect() forces the browser to synchronously recompute layout if anything is dirty — doing that on every single pointermove (which can fire dozens of times per second) is a measurable performance cost, and it also means you're trusting the *rendered* position rather than a value your own code owns.

The data attribute pattern

Instead, this snippet keeps its own running total in attributes on the element:

var x = (parseFloat(target.getAttribute('data-x')) || 0) + event.dx; target.style.transform = 'translate(' + x + 'px,' + y + 'px)'; target.setAttribute('data-x', x);

event.dx/event.dy are the incremental pixel deltas interact.js computes for you between this event and the last one. The element's *actual* position is whatever data-x/data-y currently say, applied via a CSS transform, and every subsequent event just adds its delta onto that stored number. No layout read, ever — just arithmetic on a value you already have, applied as a compositor-friendly transform.

resizable() and event.rect / event.deltaRect

The tricky part of resize is that dragging the top or left edge changes the element's effective origin, not just its size — the bottom-right corner stays put while the top-left corner moves toward or away from it. interact.js hands you both pieces of information pre-computed:

target.style.width = event.rect.width + 'px'; target.style.height = event.rect.height + 'px'; x += event.deltaRect.left; y += event.deltaRect.top;

event.rect is the panel's already-computed new bounding box for this frame — no manual math against the previous size. event.deltaRect is specifically the *change* in each edge's position since the last event, so deltaRect.left is non-zero only when the left edge itself moved (dragging the right edge alone leaves it at 0). Adding deltaRect.left/deltaRect.top onto the tracked x/y keeps the position attributes correct regardless of which edge was grabbed — dragging the bottom-right corner never touches x/y at all, while dragging the top-left corner updates both.

Constraining size with a modifier, not manual clamping

interact.modifiers.restrictSize({ min: {...}, max: {...} }) is a plugin-style modifier passed into resizable()'s config — interact.js applies the clamp internally before your move listener even runs, so event.rect never reports a size outside the allowed range in the first place. This is simpler and less error-prone than checking and clamping event.rect.width/height yourself inside the listener.

Scoping drag to the header only

draggable({ allowFrom: '.irp-panel-head', ... }) restricts which part of the element can initiate a drag — without it, grabbing anywhere on the panel body (including its resize handles) would also start a move, conflicting with resize gestures. allowFrom is a single option rather than manual event-target filtering.

Reusing it

This exact data-x/data-y plus edges/deltaRect pattern is the standard interact.js recipe for any floating, resizable UI element — dashboard widgets, modal windows, split-pane dividers. Pair it with an Interact.js Drag-Drop Kanban to see the same library's dropzone API for cross-container dragging instead of free positioning.

Build with AI

Build, Understand, Optimize, and Extend It With AI

This snippet is a compact reference for interact.js's most reused idiom, so it pays off to use an AI assistant to test your understanding of it. Paste the code into Claude and ask it to walk through, event by event, what happens to data-x, data-y, and the panel's width/height during a resize drag that starts on the top edge and ends up slightly past the top-left corner into the left edge -- tracing exactly when deltaRect.left and deltaRect.top become non-zero. Then ask what would go wrong if the resize listener wrote to target.style.left/top instead of using a transform (it would fight with the draggable's own transform-based positioning, since both would be trying to control position through different CSS properties). To extend it: ask it to add snapping to the grid shown in the background using interact.modifiers.snap, add a double-click on the header to maximize/restore the panel, or extend this into multiple independent panels that can be dragged and resized without interfering with each other.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a single floating panel that is both draggable by its header and resizable from any edge or corner, using interact.js (from a CDN, global function interact) in plain HTML, CSS, and JavaScript.

Requirements:
- The panel has a header bar (with a title) and a body, absolutely positioned inside a bounded canvas-like container with a dotted or grid background.
- Configure interact(panel).draggable({ allowFrom: '<header selector>', listeners: { move } }) so only the header can initiate a drag, not the body or resize handles.
- In the drag move listener, do NOT call getBoundingClientRect(). Instead read the running position from data-x/data-y attributes on the element (defaulting to 0), add event.dx/event.dy to them, apply the result via CSS transform: translate(x, y), and write the new values back to the data attributes.
- Configure .resizable({ edges: { left: true, right: true, top: true, bottom: true }, listeners: { move }, modifiers: [interact.modifiers.restrictSize({ min: {...}, max: {...} })] }) on the same element so it can be resized from any of the four edges (and, implicitly, the corners).
- In the resize move listener, set the element's width/height directly from event.rect.width/event.rect.height, and separately update the tracked data-x/data-y by ADDING event.deltaRect.left and event.deltaRect.top to them (applying the result via the same transform) -- this is essential so that resizing from the top or left edge repositions the panel correctly instead of only the bottom-right handle working properly.
- Show a live "width x height" readout that updates on every resize move event.
- Style it as a dark floating panel with a colored dot header (like macOS traffic lights), rounded corners, and a small visible resize handle icon in the bottom-right corner, sitting on a subtle dotted-grid dark canvas background.
- Add a code comment explaining why data-x/data-y plus transform is used instead of reading/writing left/top or getBoundingClientRect on every event.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Source Code

Requires
<div class="irp-stage">
  <div class="irp-head">
    <span class="irp-tag">interact.js · draggable + resizable</span>
    <h2>Floating Panel</h2>
    <p>Drag by the header to move it, drag any edge or the corner handle to resize it.</p>
  </div>
  <div class="irp-canvas" id="irpCanvas">
    <div class="irp-panel" id="irpPanel" data-x="0" data-y="0">
      <div class="irp-panel-head" id="irpPanelHead">
        <span class="irp-dot red"></span><span class="irp-dot yellow"></span><span class="irp-dot green"></span>
        <span class="irp-panel-title">Inspector</span>
      </div>
      <div class="irp-panel-body">
        <p>Drag the title bar to reposition. Drag any edge, or the bottom-right handle, to resize.</p>
        <div class="irp-readout" id="irpReadout">240 × 170</div>
      </div>
      <div class="irp-handle"></div>
    </div>
  </div>
</div>

Step by step

How to Use

  1. 1
    Add the interact.js CDNInclude the interact.min.js UMD build for the global interact function.
  2. 2
    Paste HTML, CSS, and JSA floating panel renders on a dotted-grid canvas at a fixed starting position.
  3. 3
    Drag the title bardraggable({ allowFrom }) moves the panel using tracked data-x/data-y attributes.
  4. 4
    Drag any edge or the corner handleresizable({ edges }) resizes it, using event.rect for size and event.deltaRect for origin shift.
  5. 5
    Watch the size readoutThe live width × height label updates on every resize move event.
  6. 6
    Try dragging the top-left cornerNotice the panel resizes and repositions simultaneously, handled by deltaRect.left/top.

Real-world uses

Common Use Cases

Dashboard widget panels
Movable, resizable inspector or chart panels in an admin or analytics UI.
Design tool floating palettes
Tool palettes and property inspectors that users can reposition and resize.
Multi-window web apps
Lightweight in-page "windows" for apps that mimic a desktop environment.
Configurable split views
A base for resizable content panes without a heavier layout library.
Teaching interact.js fundamentals
A clear reference for the data-x/data-y and deltaRect idioms used throughout the library.
Prototyping IDE-style layouts
Quick scaffolding for panel-based tools before committing to a full layout engine.

Got questions?

Frequently Asked Questions

Reading getBoundingClientRect() forces a synchronous layout recalculation, which is expensive to do on every pointermove event. Reading back a CSS transform string would require parsing it. Storing the running x/y as plain numbers in data attributes avoids both -- it is pure arithmetic on values your own code already owns, updated by simply adding event.dx/event.dy each move.

event.rect is the panel's full new bounding box for this event -- its final width, height, left, and top after this drag step. event.deltaRect is specifically the CHANGE in each edge's position since the previous event -- so deltaRect.left is only non-zero when the left edge itself was the one being dragged. That distinction is why deltaRect.left/top, not rect.left/top, is what gets added onto the tracked data-x/data-y.

Resizing from the bottom-right corner only changes width and height -- the top-left corner (the panel's effective origin) does not move. event.deltaRect.left and .top are both 0 in that case, so adding them onto x and y is a no-op, and only target.style.width/height change.

It is passed as a modifier in the resizable() config's modifiers array. interact.js applies size modifiers internally before your move listener runs, clamping event.rect.width/height to the given min/max before you ever read them -- so the listener code never needs its own clamping logic.

allowFrom restricts which part of the element can START a drag gesture -- here, only the header, so grabbing the panel body or its resize handle does not also move the whole panel. resizable() instead relies on its edges configuration and interact.js's own edge-detection margin around the element's border to decide when a resize (rather than a drag) should start, so it does not need an allowFrom equivalent.

Call interact(ref.current).draggable(...).resizable(...) once inside a useEffect (React) or onMounted (Vue) against the panel's DOM ref, keeping data-x/data-y as real DOM attributes exactly as in vanilla JS -- interact.js operates directly on the DOM node, so there is nothing framework-specific about the configuration itself, only where you attach it.