webdevpuneet.com

Interact.js Resizable Panel — Draggable + Resizable Snippet

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

CategoryLayouts

Copy & paste

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>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

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.

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
Build movable, resizable inspector or chat panels, with drag limited to the title bar through allowFrom.
Design tool palettes
Create floating property and tool palettes for design tools that can be resized from any edge or corner by the user.
Multi-window web apps
Provide lightweight in-page windows, with position stored in data-x and data-y so no layout is read on every frame.
Configurable split views
Use as a base for resizable content panes, applying event.deltaRect.left and top so left and top resizes shift the tracked origin correctly.
Interact.js and IDE layout learning
Learn how drag and resize avoid corrupting each other's idea of position, and prototype IDE-style panel layouts quickly.

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.