GODRICH
#css-animation#button#loader#transition#scroll#dashboard#ui#design-trends

Resize Handle UI — 9 Free-Transform Patterns

Among nine resize handle patterns, one spins an object as its rotate handle arcs, one snaps scattered icons into a grid on one click, and one grows its

Auto-plays · click a tile to jump to its section · all nine in one zip

This toolkit gathers nine of them on one canvas scene: drag, a corner handle, rotate, two-finger touch, a background-grid snap, one-click auto-arrange, front/back ordering, arrow-key nudge, and a PNG export preview. Each one is a self-drawn CSS/SVG object — no external images, brand marks, or photographed people anywhere in the nine demos — driven by a small, named calculation function you can read start to finish in the code section below.

Demo

Whole-Object Drag Move, No Handle Needed
Open in a new tab

01 · Whole-Object Drag Move, No Handle Needed — Measured area 11.417% · moving frames 14/23Original by GODRICH · SCSS and React ship in the zip · See the password

applyTransformonPointerMoveeaseOutCubic

Pattern 01 above needs no handle at all — press anywhere on the object's body and it follows the pointer until release. The other eight sit further down, each next to its own code block.

Anatomy of nine patterns — where the one layer of calculation runs

# Pattern Cumulative area Avg. intensity Frames moved
01 Whole-object drag 11.42% 126.0 14 of 23
02 Corner resize handle 14.39% 75.6 16 of 23
03 Rotate handle 9.46% 86.4 19 of 23
04 Two-finger pinch/rotate 9.61% 82.8 23 of 23
05 Grid snap 6.41% 136.0 10 of 23
06 Auto-arrange 14.91% 85.8 8 of 23
07 Z-order swap 6.11% 88.4 9 of 23
08 Keyboard nudge 1.34% 158.8 15 of 23
09 PNG export preview 10.89% 47.5 23 of 23

Auto-arrange (06) covers the most screen area of the nine; the keyboard nudge (08) covers the least, and its average pixel-change intensity of 158.8 is the highest of the set — a one-pixel step reads as a sharp, concentrated change rather than a soft one. Pattern 09 sits at the other end, with the lowest average intensity at 47.5. Patterns 04 and 09 each moved in all 23 of 23 measured frame pairs, while 06's auto-arrange finishes its move in as few as 8.

Live pointer-driven transform — 01, 02, 03

Patterns 01, 02, and 03 all track a single pointer continuously and recompute one transform value on every pointermove. applyTransform (01) clamps a translate pair against the frame edges, applyScale (02) clamps against MIN_SCALE/MAX_SCALE while anchoring at the opposite corner via cornerAnchorRect, and applyRotate (03) turns an atan2 angle from angleFromPointer into a mod360-wrapped rotation. None of these wait for pointerup to show the change — the transform commits mid-drag, frame by frame.

Two-input and snap geometry — 04, 05, 06

Patterns 04, 05, and 06 each compute a target from more than one raw coordinate before touching the DOM. computeFromFingers (04) turns two simulated pointer positions into a distance ratio and an angle difference in the same pass. nearestGrid (05) rounds a raw coordinate to the nearest multiple of CELL_PX, then compares the distance against CORNER_SNAP_RADIUS_PX. gridTarget (06) ranks every scattered chip into a row/column cell that animateTo eases toward with easeOutCubic.

Discrete stepped commits — 07, 08, 09

Patterns 07, 08, and 09 don't track a continuous path at all — each commits one discrete state change per interaction. swapNeighbor (07) moves an id one slot inside an order array and remaps z-index from that array's positions. computeStep (08) turns held time into an acceleration multiplier that moveBy applies as one fixed jump per key-repeat. generatePreview (09) redraws the selection through a roundedRectPath and encodes the result with canvas.toDataURL in one pass, with no animation loop involved.

Code per pattern

Every block below is copied straight out of its demo's own script — nothing retyped, nothing simplified.

01Whole-Object Drag Move, No Handle Needed

Pressing and dragging anywhere on a selected object's body moves it and its bounding selection box together along the pointer, stopping exactly where it's released. Reach for it when repositioning stickers or icons on a canvas, where a dedicated handle would only get in the way.

Whole-Object Drag Move, No Handle Needed
Open in a new tab

01 · Whole-Object Drag Move, No Handle Needed — Measured area 11.417% · moving frames 14/23Original by GODRICH · SCSS and React ship in the zip · See the password

applyTransformonPointerMoveeaseOutCubic
  function onPointerDown(e) {
    human();
    dragging = true;
    el.setPointerCapture(e.pointerId);
    startX = e.clientX; startY = e.clientY;
    originTx = tx; originTy = ty;
    el.classList.add('isDragging');
  }
  function onPointerMove(e) {
    if (!dragging) return;
    var dx = e.clientX - startX;
    var dy = e.clientY - startY;
    applyTransform(clamp(originTx + dx, -110, 110), clamp(originTy + dy, -55, 55));
  }
  function onPointerUp(e) {
    if (!dragging) return;
    dragging = false;
    el.classList.remove('isDragging');
    try { el.releasePointerCapture(e.pointerId); } catch (err) {}
  }

02Aspect-Locked Resize From Four Corner Handles

Small square handles sit on each of the selection box's four corners, and dragging one resizes the object larger or smaller while keeping its aspect ratio, anchored at the opposite corner. This corner resize handle is a different demo from 250's panel-corner-resize-grip, which resizes a layout panel's own width and height — this one is a border handle on a single canvas object, not a panel, and it's the pattern to reach for when sizing an image object on a moodboard or business-card mockup.

Aspect-Locked Resize From Four Corner Handles
Open in a new tab

02 · Aspect-Locked Resize From Four Corner Handles — Measured area 14.389% · moving frames 16/23Original by GODRICH · SCSS and React ship in the zip · See the password

applyScalecornerAnchorRecthopScale
  function applyScale(corner, s) {
    scale = clamp(s, MIN_SCALE, MAX_SCALE);
    subject.style.transformOrigin = ORIGIN_BY_CORNER[corner];
    subject.style.transform = 'scale(' + scale.toFixed(3) + ')';
    rp.textContent = Math.round(scale * 100);
    handles.forEach(function (h) { h.setAttribute('aria-valuenow', String(Math.round(scale * 100))); });
  }

  function human() {
    if (scene.classList.contains('is-demo')) {
      scene.classList.remove('is-demo');
      timers.forEach(function (t) { clearTimeout(t); clearInterval(t); });
      timers = [];
    }
  }

  function cornerAnchorRect(corner) {
    var r = frame.getBoundingClientRect();
    switch (corner) {
      case 'tl': return { x: r.right, y: r.bottom };
      case 'tr': return { x: r.left, y: r.bottom };
      case 'bl': return { x: r.right, y: r.top };
      default: return { x: r.left, y: r.top };
    }
  }

03Rotate Handle Stemming From the Selection Box's Top

A short line extends from the top-center of the selection box to a round handle, and dragging that handle along a circular path spins the object around the selection box's center by that angle. It's built for tilting a sticker or stamp-style object at an angle.

Rotate Handle Stemming From the Selection Box's Top
Open in a new tab

03 · Rotate Handle Stemming From the Selection Box's Top — Measured area 9.46% · moving frames 19/23Original by GODRICH · SCSS and React ship in the zip · See the password

applyRotateangleFromPointermod360
  function applyRotate(deg) {
    angle = mod360(deg);
    rotator.style.transform = 'rotate(' + angle.toFixed(1) + 'deg)';
    rd.textContent = Math.round(angle);
    handle.setAttribute('aria-valuenow', String(Math.round(angle)));
  }

  function human() {
    if (scene.classList.contains('is-demo')) {
      scene.classList.remove('is-demo');
      timers.forEach(function (t) { clearTimeout(t); clearInterval(t); });
      timers = [];
    }
  }

  // Rotate handle = atan2 angle → rotate. From the frame's center (a fixed, non-rotating element)
  // to the pointer, the absolute angle is recomputed every frame and snapped by rounding to a
  // multiple of ROTATE_SNAP_DEG.
  function angleFromPointer(e) {
    var r = frame.getBoundingClientRect();
    var cx = r.left + r.width / 2;
    var cy = r.top + r.height / 2;
    var raw = Math.atan2(e.clientY - cy, e.clientX - cx) * 180 / Math.PI;
    return mod360(raw + 90);
  }

04Two-Finger Scale and Rotate on the Selected Object Only

On a touchscreen, with one object selected, spreading or pinching two fingers over it scales just that object, and twisting the two fingers at the same time layers a rotation into the same gesture. This is a different demo from 247's gesture-pinch-zoom-pan, which zooms and pans the whole screen — this one applies only to the single selected object, not the viewport, which makes it the pattern for fine-tuning a collage object with fingertips on mobile.

Two-Finger Scale and Rotate on the Selected Object Only
Open in a new tab

04 · Two-Finger Scale and Rotate on the Selected Object Only — Measured area 9.606% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

computeFromFingersplaceFingerbindFinger
  function placeFinger(el, pos) {
    var r = frame.getBoundingClientRect();
    el.style.left = (pos.x * r.width) + 'px';
    el.style.top = (pos.y * r.height) + 'px';
  }

  // Distance/angle between two pointers → scale and rotate. In place of two real fingers, two
  // simulated pointer events (finger A/B) drive the distance ratio and angle difference applied
  // straight to subjectShape.
  function computeFromFingers() {
    var r = frame.getBoundingClientRect();
    var ax = posA.x * r.width, ay = posA.y * r.height;
    var bx = posB.x * r.width, by = posB.y * r.height;
    var d = Math.hypot(bx - ax, by - ay);
    var a = Math.atan2(by - ay, bx - ax) * 180 / Math.PI;
    if (baseDist === null) { baseDist = d; baseAngle = a; }
    var scale = clamp(d / baseDist, MIN_SCALE, MAX_SCALE);
    var rotate = a - baseAngle;
    subject.style.transform = 'translate(-50%,-50%) rotate(' + rotate.toFixed(1) + 'deg) scale(' + scale.toFixed(3) + ')';
    rp.textContent = Math.round(scale * 100);
    rr.textContent = Math.round(rotate);
    placeFinger(fingerA, posA);
    placeFinger(fingerB, posB);
  }

05Snap-to-Background-Grid While Dragging

While an object is being dragged, once its edge comes within a set distance of an intersection on the faint background grid, it gets pulled the rest of the way onto that intersection before the pointer is even released. It's a different demo from 250's panel-snap-guides, which are fixed guide lines at the 25, 50, and 75 percent ratio marks inside a single split panel — this one snaps to intersections on a many-cell background grid instead of a ratio mark, and it's the fit for placing multiple icons at even spacing.

Snap-to-Background-Grid While Dragging
Open in a new tab

05 · Snap-to-Background-Grid While Dragging — Measured area 6.406% · moving frames 10/23Original by GODRICH · SCSS and React ship in the zip · See the password

nearestGridapplyPositionhopTo
  // Compares the object's corner (top-left) against the nearest grid intersection (a multiple of
  // CELL_PX); if that distance is within CORNER_SNAP_RADIUS_PX, it snaps to the intersection even
  // before the pointer is released — it latches onto one fixed background grid, not onto other
  // objects (a different demo from 250's panel-snap-guides).
  function nearestGrid(v) { return Math.round(v / CELL_PX) * CELL_PX; }

  function applyPosition(rawX, rawY) {
    var nx = nearestGrid(rawX);
    var ny = nearestGrid(rawY);
    var d = Math.hypot(rawX - nx, rawY - ny);
    var snapped = d <= CORNER_SNAP_RADIUS_PX;
    tx = clamp(snapped ? nx : rawX, 0, frameW - 44);
    ty = clamp(snapped ? ny : rawY, 0, frameH - 32);
    draggable.style.transform = 'matrix(1,0,0,1,' + tx.toFixed(1) + ',' + ty.toFixed(1) + ')';
    if (snapped) {
      guideDot.style.transform = 'matrix(1,0,0,1,' + nx.toFixed(1) + ',' + ny.toFixed(1) + ')';
      guideDot.classList.add('isActive');
    } else {
      guideDot.classList.remove('isActive');
    }
    rs.textContent = snapped ? 'snap' : 'free';
    draggable.classList.toggle('isSnapped', snapped);
  }

06One-Click Auto-Arrange of Scattered Objects Into a Grid

Clicking one button sends every scattered object gliding at once from wherever it sat into a properly ranked grid cell, all arriving lined up in rows and columns together. It's the pattern for tidying scattered moodboard icons right before a presentation.

One-Click Auto-Arrange of Scattered Objects Into a Grid
Open in a new tab

06 · One-Click Auto-Arrange of Scattered Objects Into a Grid — Measured area 14.908% · moving frames 8/23Original by GODRICH · SCSS and React ship in the zip · See the password

gridTargetanimateTotoggleArrange
  function animateTo(targets, onDone) {
    var from = current.map(function (p) { return { x: p.x, y: p.y }; });
    var start = performance.now();
    function step(now) {
      var p = Math.min(1, (now - start) / HOP_MS);
      var e = easeOutCubic(p);
      for (var i = 0; i < chips.length; i++) {
        current[i].x = from[i].x + (targets[i].x - from[i].x) * e;
        current[i].y = from[i].y + (targets[i].y - from[i].y) * e;
      }
      paint();
      if (p < 1) requestAnimationFrame(step);
      else if (onDone) onDone();
    }
    requestAnimationFrame(step);
  }

  function toggleArrange() {
    arranged = !arranged;
    var targets = arranged ? chips.map(function (_, i) { return gridTarget(i); }) : SCATTER;
    status.textContent = arranged ? 'Arranged' : 'Scattered';
    btn.setAttribute('aria-pressed', String(arranged));
    animateTo(targets);
  }

07Sending Overlapping Objects Forward or Back One Step

Selecting an object that sits underneath in a stack and choosing bring-forward instantly flips it above the object that was covering it, reordering the overlap right on screen. It's built for bringing a sticker buried behind a card to the front.

Sending Overlapping Objects Forward or Back One Step
Open in a new tab

07 · Sending Overlapping Objects Forward or Back One Step — Measured area 6.113% · moving frames 9/23Original by GODRICH · SCSS and React ship in the zip · See the password

swapNeighborbringToFrontpopCard
  // Array reorder → z-index: the selected id moves one slot inside the order array, and that
  // array's position is remapped straight to z-index — z comes out as the result of an array
  // operation, not a hand-picked style value.
  function swapNeighbor(dir) {
    var idx = order.indexOf(selectedId);
    var next = idx + dir;
    if (next < 0 || next >= order.length) return;
    var tmp = order[idx];
    order[idx] = order[next];
    order[next] = tmp;
    applyZ();
    popCard(cards[selectedId]);
  }

  function bringToFront(id) {
    var idx = order.indexOf(id);
    order.splice(idx, 1);
    order.push(id);
    selectedId = id;
    applyZ();
    popCard(cards[selectedId]);
  }

08One-Pixel Nudge Move With Arrow Keys

With an object selected, each single arrow-key press moves it a tiny fixed distance in that direction, and holding the key down keeps that same small step for the first few moments before the distance per step gradually grows. Use it for fixing a slightly misaligned object down to the pixel.

One-Pixel Nudge Move With Arrow Keys
Open in a new tab

08 · One-Pixel Nudge Move With Arrow Keys — Measured area 1.344% · moving frames 15/23Original by GODRICH · SCSS and React ship in the zip · See the password

computeStepmoveBysimulateHold
  // Held time (elapsed) → acceleration multiplier. It starts at 1x, grows by 1x every
  // ACCEL_WINDOW_MS, and stops at MAX_MULT — the longer the key stays down (native browser
  // key-repeat keeps calling this), the farther it travels.
  function computeStep(elapsedMs) {
    var mult = 1 + Math.min(Math.floor(elapsedMs / ACCEL_WINDOW_MS), MAX_MULT - 1);
    return { step: Math.max(1, Math.round(KEYBOARD_NUDGE_PX * mult)), mult: mult };
  }

  function moveBy(dx, dy, mult) {
    tx = clamp(tx + dx, 0, frameW - 44);
    ty = clamp(ty + dy, 0, frameH - 32);
    draggable.style.transform = 'matrix(1,0,0,1,' + tx.toFixed(0) + ',' + ty.toFixed(0) + ')';
    rm.textContent = String(mult);
  }

09Export Panel That Crops Just the Selection to PNG

Clicking the button redraws just the area inside the selection box onto a canvas, encodes it as PNG data, and shows the result as a preview image in the export panel right away — this is a simulation that stops at the preview, with no real file save or download behind it. It's the pattern for saving a finished composition as a profile image or banner once a real export endpoint exists.

Export Panel That Crops Just the Selection to PNG
Open in a new tab

09 · Export Panel That Crops Just the Selection to PNG — Measured area 10.889% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

roundedRectPathgeneratePreviewautoPreviewLoop
  // Exports the object via canvas.toDataURL: the same rotation and scale as the on-screen
  // subjectShape are reproduced with a ctx transform (mirrored arithmetic), then a roundRect path
  // is filled with a gradient to build a PNG data URL. No real file save, download, or clipboard
  // write happens — only the preview image is simulated.
  function generatePreview() {
    var w = canvas.width, h = canvas.height;
    ctx.clearRect(0, 0, w, h);
    ctx.save();
    ctx.translate(w / 2, h / 2);
    ctx.rotate(angle * Math.PI / 180);
    ctx.scale(scale, scale);
    var grad = ctx.createLinearGradient(-34, -34, 34, 34);
    grad.addColorStop(0, FILL_A);
    grad.addColorStop(1, FILL_B);
    ctx.fillStyle = grad;
    roundedRectPath(ctx, -34, -34, 68, 68, 14);
    ctx.fill();
    ctx.restore();
    img.src = canvas.toDataURL('image/png');
    status.textContent = 'Generated';
  }

Where values change the UI — snap radius, rotate snap angle, nudge acceleration

05 — CORNER_SNAP_RADIUS_PX

This constant is the distance, in pixels, that a dragged corner has to come within a grid intersection before nearestGrid decides the drag should snap to grid instead of following the pointer freely. A small value keeps free movement wide and snapping rare; a larger one below turns it into a magnet that pulls from further away.

03 — ROTATE_SNAP_DEG

Every angle angleFromPointer returns is rounded to the nearest multiple of this value before applyRotate uses it. A wide step gives a coarse, click-into-place rotation; a narrow one lets the handle settle on far more intermediate angles.

08 — KEYBOARD_NUDGE_PX

computeStep multiplies this base distance by the current acceleration multiplier on every key-repeat. Raising it changes how far even the very first, un-accelerated press moves the object, before acceleration ever kicks in.

Where it breaks — accessibility, reduced-motion, keyboard focus

Pattern 04's two touch handles are marked up with role="slider" and a static aria-valuenow="50" in the HTML — but unlike patterns 02 and 03, which call .setAttribute('aria-valuenow', value) inside their own apply function on every change, nothing in pattern 04's script ever updates that attribute again after the page loads. A screen reader landing on either finger handle after a pinch gesture still announces the original value it started with, no matter how far the object has actually been scaled or rotated. The valid ranges those sliders expose differ by pattern, too — 02's four corner handles report 50 to 180 for a percent scale, 03's rotate handle spans the full 0 to 360 degrees, and 04's two finger points sit on a plain 0 to 100 scale — but only 02 and 03 keep that number honest as the value actually changes.

What reduced motion leaves behind

The full markup, script, and token-driven SCSS behind all nine patterns — the drag physics, the snap math, the auto-arrange easing, and the export canvas code included — ship in the zip below, unlocked with the password 8eh278nq, and reading it end to end is the only way to see exactly which timer each pattern below turns off.

  • 01 — the idle auto-hop between waypoints stops; a real pointer drag still moves the object the instant it's pressed.
  • 02 — the demo's own scripted resize cycle stops; dragging a corner handle still rescales immediately.
  • 03 — the automatic sweep around the dial stops; dragging the rotate handle still turns the object at once.
  • 04 — the simulated pinch/rotate loop stops; real two-finger input still scales and rotates without delay.
  • 05 — the auto-drag-and-snap demonstration stops; a manual drag still snaps to the grid the same way.
  • 06 — nothing about toggleArrange itself is time-based beyond the animation, so clicking still re-sorts the grid instantly, without the eased glide.
  • 07 — the scripted bring-forward/back cycle stops; clicking a card still reorders it immediately.
  • 08 — unaffected either way: pattern 08 never calls requestAnimationFrame in the first place, so a key press moves the object by the same amount regardless of the setting.
  • 09 — the automatic preview refresh (AUTO_PREVIEW_MS) stops; clicking export still redraws and encodes the PNG preview on demand.

Three of those constants are worth turning into a live variant to feel exactly how each one changes the interaction:

Three variants and accessibility

Variant Changed value What it feels like
Widen the corner snap magnet radius 05 CORNER_SNAP_RADIUS_PX 6 → 16 Snapping triggers from farther away from a grid intersection, shrinking the free-drag zone and making the object latch on more readily.
Narrow the rotate handle's angle-snap step 03 ROTATE_SNAP_DEG 15 → 5 Each jump when dragging the handle shrinks, allowing finer, more granular rotation control.
Increase the keyboard nudge step size 08 KEYBOARD_NUDGE_PX 1 → 8 Even the very first key press moves much further, trading pixel-perfect precision for faster, larger repositioning.

Across the nine demos, the handles that behave like sliders (02, 03, 04) expose role="slider" with aria-valuemin/aria-valuemax/aria-valuenow, the reorderable cards in 07 use role="button" with tabindex="0" so every card is keyboard-reachable, and the status text in 06, 07, and 09 sits inside an aria-live="polite" region so a screen reader hears "Arranged," a reorder, or "Generated" without needing to refocus anything.

FAQ

Does pattern 04's two-finger gesture work the same as a browser's native pinch zoom?

No — this pinch zoom is scoped entirely to the selected canvas object, computed from two simulated pointer positions in computeFromFingers. It never touches the page's own zoom level or scrolls the viewport, which is what makes it a different demo from 247's gesture-pinch-zoom-pan.

Does pattern 09 actually save a PNG file to disk?

No. generatePreview calls canvas.toDataURL('image/png') and points a preview <img> at the result — that's a simulation that stops at the preview image. Nothing is written to disk, downloaded, or copied to the clipboard.

Why does the keyboard nudge in pattern 08 speed up the longer a key is held?

computeStep turns the held duration into an acceleration multiplier, capped at MAX_MULT, so the same arrow key moves the object by a growing number of pixels the longer native key-repeat keeps firing — useful for covering distance fast, then easing off near the target by tapping instead of holding.

Sources: Pointer events · CSS transform · touch-action · HTMLCanvasElement.toDataURL · keydown event

Related reading: 9 Resizable Split Pane Layouts With No Library · Image Crop JavaScript: 9 Canvas Editor UIs · 9 Mobile Touch Gestures That Follow Your Finger

Enter the archive password

The password is inside this article. You will find it as you read.